Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 29 Jul 2015 13:49:24 -0400
changeset 255225 62cd40885e9362dc726f681675b64aa9577982aa
parent 255195 eb881eabba8e82719db0afe8e02fcb9a53199cf2 (current diff)
parent 255224 130ef92fa5a74132d9d30a20bade21c5736457f5 (diff)
child 255231 7910e4e1015a8843580a63bfed4097945c2df2d1
child 255255 b567800c8964fce52f9bd60c28b223662d4e1c2a
child 255292 ee17e2b7ede8b9feaf161280a10a7618e043d4bc
push id29131
push userryanvm@gmail.com
push dateWed, 29 Jul 2015 17:49:23 +0000
treeherdermozilla-central@62cd40885e93 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone42.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 fx-team to m-c. a=merge
browser/devtools/styleinspector/test/browser_ruleview_override.js
--- a/browser/base/content/browser-trackingprotection.js
+++ b/browser/base/content/browser-trackingprotection.js
@@ -121,16 +121,19 @@ let TrackingProtection = {
     } else {
       Services.perms.add(normalizedUrl,
         "trackingprotection", Services.perms.ALLOW_ACTION);
     }
 
     // Telemetry for disable protection.
     this.eventsHistogram.add(1);
 
+    // Hide the control center.
+    document.getElementById("identity-popup").hidePopup();
+
     BrowserReload();
   },
 
   enableForCurrentPage() {
     // Remove the current host from the 'trackingprotection' consumer
     // of the permission manager. This effectively removes this host
     // from the tracking protection allowlist.
     let normalizedUrl = Services.io.newURI(
@@ -141,16 +144,19 @@ let TrackingProtection = {
       PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
     } else {
       Services.perms.remove(normalizedUrl, "trackingprotection");
     }
 
     // Telemetry for enable protection.
     this.eventsHistogram.add(2);
 
+    // Hide the control center.
+    document.getElementById("identity-popup").hidePopup();
+
     BrowserReload();
   },
 
   showIntroPanel: Task.async(function*() {
     let mm = gBrowser.selectedBrowser.messageManager;
     let brandBundle = document.getElementById("bundle_brand");
     let brandShortName = brandBundle.getString("brandShortName");
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1134,17 +1134,19 @@ var gBrowserInit = {
     mm.addMessageListener("PageVisibility:Show", function(message) {
       if (message.target == gBrowser.selectedBrowser) {
         setTimeout(pageShowEventHandlers, 0, message.data.persisted);
       }
     });
 
     gBrowser.addEventListener("AboutTabCrashedLoad", function(event) {
 #ifdef MOZ_CRASHREPORTER
-      TabCrashReporter.onAboutTabCrashedLoad(gBrowser.getBrowserForDocument(event.target));
+      TabCrashReporter.onAboutTabCrashedLoad(gBrowser.getBrowserForDocument(event.target), {
+        crashedTabCount: SessionStore.crashedTabCount,
+      });
 #endif
     }, false, true);
 
     gBrowser.addEventListener("AboutTabCrashedMessage", function(event) {
       let ownerDoc = event.originalTarget;
 
       if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) {
         return;
@@ -1166,21 +1168,17 @@ var gBrowserInit = {
       switch (event.detail.message) {
       case "closeTab":
         gBrowser.removeTab(tab, { animate: true });
         break;
       case "restoreTab":
         SessionStore.reviveCrashedTab(tab);
         break;
       case "restoreAll":
-        for (let browserWin of browserWindows()) {
-          for (let tab of browserWin.gBrowser.tabs) {
-            SessionStore.reviveCrashedTab(tab);
-          }
-        }
+        SessionStore.reviveAllCrashedTabs();
         break;
       }
     }, false, true);
 
     let uriToLoad = this._getUriToLoad();
     if (uriToLoad && uriToLoad != "about:blank") {
       if (uriToLoad instanceof Ci.nsISupportsArray) {
         let count = uriToLoad.Count();
@@ -2406,18 +2404,20 @@ function BrowserViewSource(browser) {
     outerWindowID: browser.outerWindowID,
     URL: browser.currentURI.spec,
   });
 }
 
 // doc - document to use for source, or null for this window's document
 // initialTab - name of the initial tab to display, or null for the first tab
 // imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted
-function BrowserPageInfo(doc, initialTab, imageElement) {
-  var args = {doc: doc, initialTab: initialTab, imageElement: imageElement};
+// frameOuterWindowID - the id of the frame that the context menu opened in; can be null/omitted
+function BrowserPageInfo(doc, initialTab, imageElement, frameOuterWindowID) {
+  var args = {doc: doc, initialTab: initialTab, imageElement: imageElement,
+              frameOuterWindowID: frameOuterWindowID};
   var windows = Services.wm.getEnumerator("Browser:page-info");
 
   var documentURL = doc ? doc.location : window.gBrowser.selectedBrowser.contentDocumentAsCPOW.location;
 
   // Check for windows matching the url
   while (windows.hasMoreElements()) {
     var currentWindow = windows.getNext();
     if (currentWindow.closed) {
@@ -6628,16 +6628,17 @@ var gIdentityHandler = {
   IDENTITY_MODE_UNKNOWN                                : "unknownIdentity",  // No trusted identity information
   IDENTITY_MODE_USES_WEAK_CIPHER                       : "unknownIdentity weakCipher",  // SSL with RC4 cipher suite or SSL3
   IDENTITY_MODE_MIXED_DISPLAY_LOADED                   : "unknownIdentity mixedContent mixedDisplayContent",  // SSL with unauthenticated display content
   IDENTITY_MODE_MIXED_ACTIVE_LOADED                    : "unknownIdentity mixedContent mixedActiveContent",  // SSL with unauthenticated active (and perhaps also display) content
   IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED    : "unknownIdentity mixedContent mixedDisplayContentLoadedActiveBlocked",  // SSL with unauthenticated display content; unauthenticated active content is blocked.
   IDENTITY_MODE_MIXED_ACTIVE_BLOCKED                   : "verifiedDomain mixedContent mixedActiveBlocked",  // SSL with unauthenticated active content blocked; no unauthenticated display content
   IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED        : "verifiedIdentity mixedContent mixedActiveBlocked",  // SSL with unauthenticated active content blocked; no unauthenticated display content
   IDENTITY_MODE_CHROMEUI                               : "chromeUI",         // Part of the product's UI
+  IDENTITY_MODE_FILE_URI                               : "fileURI",  // File path
 
   // Cache the most recent SSLStatus and Location seen in checkIdentity
   _lastStatus : null,
   _lastUri : null,
   _mode : "unknownIdentity",
 
   // smart getters
   get _identityPopup () {
@@ -6843,17 +6844,30 @@ var gIdentityHandler = {
       } else if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
         this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED);
       } else if (state & nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
         this.setMode(this.IDENTITY_MODE_MIXED_DISPLAY_LOADED);
       } else {
         this.setMode(this.IDENTITY_MODE_USES_WEAK_CIPHER);
       }
     } else {
-      this.setMode(this.IDENTITY_MODE_UNKNOWN);
+      // Create a channel for the sole purpose of getting the resolved URI
+      // of the request to determine if it's loaded from the file system.
+      let resolvedURI = NetUtil.newChannel({uri,loadUsingSystemPrincipal:true}).URI;
+      if (resolvedURI.schemeIs("jar")) {
+        // Given a URI "jar:<jar-file-uri>!/<jar-entry>"
+        // create a new URI using <jar-file-uri>!/<jar-entry>
+        resolvedURI = NetUtil.newURI(resolvedURI.path);
+      }
+
+      if (resolvedURI.schemeIs("file")) {
+        this.setMode(this.IDENTITY_MODE_FILE_URI);
+      } else {
+        this.setMode(this.IDENTITY_MODE_UNKNOWN);
+      }
     }
 
     // Show the doorhanger when:
     // - mixed active content is blocked
     // - mixed active content is loaded (detected but not blocked)
     // - tracking content is blocked
     // - tracking content is not blocked
     if (state &
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -34,16 +34,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils",
   "resource:///modules/PlacesUIUtils.jsm");
 XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuChild();
 });
 
+XPCOMUtils.defineLazyModuleGetter(this, "Feeds", "resource:///modules/Feeds.jsm");
+
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
 
 addMessageListener("ContextMenu:DoCustomCommand", function(message) {
   PageMenuChild.executeMenu(message.data);
@@ -835,8 +837,401 @@ addMessageListener("ContextMenu:SetAsDes
       Cu.reportError(e);
       disable = true;
     }
   }
 
   if (disable)
     sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
 });
+
+let pageInfoListener = {
+
+  init: function(chromeGlobal) {
+    chromeGlobal.addMessageListener("PageInfo:getData", this, false, true);
+  },
+
+  receiveMessage: function(message) {
+    this.imageViewRows = [];
+    this.frameList = [];
+    this.strings = message.data.strings;
+
+    let frameOuterWindowID = message.data.frameOuterWindowID;
+
+    // If inside frame then get the frame's window and document.
+    if (frameOuterWindowID) {
+      this.window = Services.wm.getOuterWindowWithId(frameOuterWindowID);
+      this.document = this.window.document;
+    }
+    else {
+      this.document = content.document;
+      this.window = content.window;
+    }
+
+    let pageInfoData = {metaViewRows: this.getMetaInfo(), docInfo: this.getDocumentInfo(),
+                        feeds: this.getFeedsInfo(), windowInfo: this.getWindowInfo()};
+    sendAsyncMessage("PageInfo:data", pageInfoData);
+
+    // Separate step so page info dialog isn't blank while waiting for this to finish.
+    this.getMediaInfo();
+
+    // Send the message after all the media elements have been walked through.
+    let pageInfoMediaData = {imageViewRows: this.imageViewRows};
+
+    this.imageViewRows = null;
+    this.frameList = null;
+    this.strings = null;
+    this.window = null;
+    this.document = null;
+
+    sendAsyncMessage("PageInfo:mediaData", pageInfoMediaData);
+  },
+
+  getMetaInfo: function() {
+    let metaViewRows = [];
+
+    // Get the meta tags from the page.
+    let metaNodes = this.document.getElementsByTagName("meta");
+
+    for (let metaNode of metaNodes) {
+      metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"),
+                        metaNode.content]);
+    }
+
+    return metaViewRows;
+  },
+
+  getWindowInfo: function() {
+    let windowInfo = {};
+    windowInfo.isTopWindow = this.window == this.window.top;
+
+    let hostName = null;
+    try {
+      hostName = this.window.location.host;
+    }
+    catch (exception) { }
+
+    windowInfo.hostName = hostName;
+    return windowInfo;
+  },
+
+  getDocumentInfo: function() {
+    let docInfo = {};
+    docInfo.title = this.document.title;
+    docInfo.location = this.document.location.toString();
+    docInfo.referrer = this.document.referrer;
+    docInfo.compatMode = this.document.compatMode;
+    docInfo.contentType = this.document.contentType;
+    docInfo.characterSet = this.document.characterSet;
+    docInfo.lastModified = this.document.lastModified;
+
+    let documentURIObject = {};
+    documentURIObject.spec = this.document.documentURIObject.spec;
+    documentURIObject.originCharset = this.document.documentURIObject.originCharset;
+    docInfo.documentURIObject = documentURIObject;
+
+    docInfo.isContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(content);
+
+    return docInfo;
+  },
+
+  getFeedsInfo: function() {
+    let feeds = [];
+    // Get the feeds from the page.
+    let linkNodes = this.document.getElementsByTagName("link");
+    let length = linkNodes.length;
+    for (let i = 0; i < length; i++) {
+      let link = linkNodes[i];
+      if (!link.href) {
+        continue;
+      }
+      let rel = link.rel && link.rel.toLowerCase();
+      let rels = {};
+
+      if (rel) {
+        for each (let relVal in rel.split(/\s+/)) {
+          rels[relVal] = true;
+        }
+      }
+
+      if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) {
+        let type = Feeds.isValidFeed(link, this.document.nodePrincipal, "feed" in rels);
+        if (type) {
+          type = this.strings[type] || this.strings["application/rss+xml"];
+          feeds.push([link.title, type, link.href]);
+        }
+      }
+    }
+    return feeds;
+  },
+
+  // Only called once to get the media tab's media elements from the content page.
+  // The actual work is done with a TreeWalker that calls doGrab() once for
+  // each element node in the document.
+  getMediaInfo: function()
+  {
+    this.goThroughFrames(this.document, this.window);
+    this.processFrames();
+  },
+
+  goThroughFrames: function(aDocument, aWindow)
+  {
+    this.frameList.push(aDocument);
+    if (aWindow && aWindow.frames.length > 0) {
+      let num = aWindow.frames.length;
+      for (let i = 0; i < num; i++) {
+        this.goThroughFrames(aWindow.frames[i].document, aWindow.frames[i]);  // recurse through the frames
+      }
+    }
+  },
+
+  processFrames: function()
+  {
+    if (this.frameList.length) {
+      let doc = this.frameList[0];
+      let iterator = doc.createTreeWalker(doc, content.NodeFilter.SHOW_ELEMENT, elem => this.grabAll(elem));
+      this.frameList.shift();
+      this.doGrab(iterator);
+    }
+  },
+
+  /**
+   * This function's previous purpose in pageInfo.js was to get loop through 500 elements at a time.
+   * The iterator filter will filter for media elements.
+   * #TODO Bug 1175794: refactor pageInfo.js to receive a media element at a time
+   * from messages and continually update UI.
+   */
+  doGrab: function(iterator)
+  {
+    while (true)
+    {
+      if (!iterator.nextNode()) {
+        this.processFrames();
+        return;
+      }
+    }
+  },
+
+  grabAll: function(elem)
+  {
+    // Check for images defined in CSS (e.g. background, borders), any node may have multiple.
+    let computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, "");
+
+    let addImage = (url, type, alt, elem, isBg) => {
+      let element = this.serializeElementInfo(url, type, alt, elem, isBg);
+      this.imageViewRows.push([url, type, alt, element, isBg]);
+    };
+
+    if (computedStyle) {
+      let addImgFunc = (label, val) => {
+        if (val.primitiveType == content.CSSPrimitiveValue.CSS_URI) {
+          addImage(val.getStringValue(), label, this.strings.notSet, elem, true);
+        }
+        else if (val.primitiveType == content.CSSPrimitiveValue.CSS_STRING) {
+          // This is for -moz-image-rect.
+          // TODO: Reimplement once bug 714757 is fixed.
+          let strVal = val.getStringValue();
+          if (strVal.search(/^.*url\(\"?/) > -1) {
+            let url = strVal.replace(/^.*url\(\"?/,"").replace(/\"?\).*$/,"");
+            addImage(url, label, this.strings.notSet, elem, true);
+          }
+        }
+        else if (val.cssValueType == content.CSSValue.CSS_VALUE_LIST) {
+          // Recursively resolve multiple nested CSS value lists.
+          for (let i = 0; i < val.length; i++) {
+            addImgFunc(label, val.item(i));
+          }
+        }
+      };
+
+      addImgFunc(this.strings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image"));
+      addImgFunc(this.strings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source"));
+      addImgFunc(this.strings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image"));
+      addImgFunc(this.strings.mediaCursor, computedStyle.getPropertyCSSValue("cursor"));
+    }
+
+    // One swi^H^H^Hif-else to rule them all.
+    if (elem instanceof content.HTMLImageElement) {
+      addImage(elem.src, this.strings.mediaImg,
+               (elem.hasAttribute("alt")) ? elem.alt : this.strings.notSet, elem, false);
+    }
+    else if (elem instanceof content.SVGImageElement) {
+      try {
+        // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI
+        //       or the URI formed from the baseURI and the URL is not a valid URI.
+        let href = makeURLAbsolute(elem.baseURI, elem.href.baseVal);
+        addImage(href, this.strings.mediaImg, "", elem, false);
+      } catch (e) { }
+    }
+    else if (elem instanceof content.HTMLVideoElement) {
+      addImage(elem.currentSrc, this.strings.mediaVideo, "", elem, false);
+    }
+    else if (elem instanceof content.HTMLAudioElement) {
+      addImage(elem.currentSrc, this.strings.mediaAudio, "", elem, false);
+    }
+    else if (elem instanceof content.HTMLLinkElement) {
+      if (elem.rel && /\bicon\b/i.test(elem.rel)) {
+        addImage(elem.href, this.strings.mediaLink, "", elem, false);
+      }
+    }
+    else if (elem instanceof content.HTMLInputElement || elem instanceof content.HTMLButtonElement) {
+      if (elem.type.toLowerCase() == "image") {
+        addImage(elem.src, this.strings.mediaInput,
+                 (elem.hasAttribute("alt")) ? elem.alt : this.strings.notSet, elem, false);
+      }
+    }
+    else if (elem instanceof content.HTMLObjectElement) {
+      addImage(elem.data, this.strings.mediaObject, this.getValueText(elem), elem, false);
+    }
+    else if (elem instanceof content.HTMLEmbedElement) {
+      addImage(elem.src, this.strings.mediaEmbed, "", elem, false);
+    }
+
+    return content.NodeFilter.FILTER_ACCEPT;
+  },
+
+  /**
+   * Set up a JSON element object with all the instanceOf and other infomation that
+   * makePreview in pageInfo.js uses to figure out how to display the preview.
+   */
+
+  serializeElementInfo: function(url, type, alt, item, isBG)
+  {
+    // Interface for image loading content.
+    const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent;
+
+    let result = {};
+
+    let imageText;
+    if (!isBG &&
+        !(item instanceof content.SVGImageElement) &&
+        !(this.document instanceof content.ImageDocument)) {
+      imageText = item.title || item.alt;
+
+      if (!imageText && !(item instanceof content.HTMLImageElement)) {
+        imageText = this.getValueText(item);
+      }
+    }
+
+    result.imageText = imageText;
+    result.longDesc = item.longDesc;
+    result.numFrames = 1;
+
+    if (item instanceof content.HTMLObjectElement ||
+      item instanceof content.HTMLEmbedElement ||
+      item instanceof content.HTMLLinkElement) {
+      result.mimeType = item.type;
+    }
+
+    if (!result.mimeType && !isBG && item instanceof nsIImageLoadingContent) {
+      // Interface for image loading content.
+      const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent;
+      let imageRequest = item.getRequest(nsIImageLoadingContent.CURRENT_REQUEST);
+      if (imageRequest) {
+        result.mimeType = imageRequest.mimeType;
+        let image = !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) && imageRequest.image;
+        if (image) {
+          result.numFrames = image.numFrames;
+        }
+      }
+    }
+
+    // if we have a data url, get the MIME type from the url
+    if (!result.mimeType && url.startsWith("data:")) {
+      let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url);
+      if (dataMimeType)
+        result.mimeType = dataMimeType[1].toLowerCase();
+    }
+
+    result.HTMLLinkElement = item instanceof content.HTMLLinkElement;
+    result.HTMLInputElement = item instanceof content.HTMLInputElement;
+    result.HTMLImageElement = item instanceof content.HTMLImageElement;
+    result.HTMLObjectElement = item instanceof content.HTMLObjectElement;
+    result.SVGImageElement = item instanceof content.SVGImageElement;
+    result.HTMLVideoElement = item instanceof content.HTMLVideoElement;
+    result.HTMLAudioElement = item instanceof content.HTMLAudioElement;
+
+    if (!isBG) {
+      result.width = item.width;
+      result.height = item.height;
+    }
+
+    if (item instanceof content.SVGImageElement) {
+      result.SVGImageElementWidth = item.width.baseVal.value;
+      result.SVGImageElementHeight = item.height.baseVal.value;
+    }
+
+    result.baseURI = item.baseURI;
+
+    return result;
+  },
+
+  //******** Other Misc Stuff
+  // Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
+  // parse a node to extract the contents of the node
+  getValueText: function(node)
+  {
+
+    let valueText = "";
+
+    // Form input elements don't generally contain information that is useful to our callers, so return nothing.
+    if (node instanceof content.HTMLInputElement ||
+        node instanceof content.HTMLSelectElement ||
+        node instanceof content.HTMLTextAreaElement) {
+      return valueText;
+    }
+
+    // Otherwise recurse for each child.
+    let length = node.childNodes.length;
+
+    for (let i = 0; i < length; i++) {
+      let childNode = node.childNodes[i];
+      let nodeType = childNode.nodeType;
+
+      // Text nodes are where the goods are.
+      if (nodeType == content.Node.TEXT_NODE) {
+        valueText += " " + childNode.nodeValue;
+      }
+      // And elements can have more text inside them.
+      else if (nodeType == content.Node.ELEMENT_NODE) {
+        // Images are special, we want to capture the alt text as if the image weren't there.
+        if (childNode instanceof content.HTMLImageElement) {
+          valueText += " " + this.getAltText(childNode);
+        }
+        else {
+          valueText += " " + this.getValueText(childNode);
+        }
+      }
+    }
+
+    return this.stripWS(valueText);
+  },
+
+  // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
+  // Traverse the tree in search of an img or area element and grab its alt tag.
+  getAltText: function(node)
+  {
+    let altText = "";
+
+    if (node.alt) {
+      return node.alt;
+    }
+    let length = node.childNodes.length;
+    for (let i = 0; i < length; i++) {
+      if ((altText = this.getAltText(node.childNodes[i]) != undefined)) { // stupid js warning...
+        return altText;
+      }
+    }
+    return "";
+  },
+
+  // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
+  // Strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space.
+  stripWS: function(text)
+  {
+    let middleRE = /\s+/g;
+    let endRE = /(^\s+)|(\s+$)/g;
+
+    text = text.replace(middleRE, " ");
+    return text.replace(endRE, "");
+  }
+};
+pageInfoListener.init(this);
\ No newline at end of file
--- a/browser/base/content/contentSearchUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -91,20 +91,16 @@ ContentSearchUIController.prototype = {
   },
 
   get engines() {
     return this._engines;
   },
 
   set engines(val) {
     this._engines = val;
-    if (!this._table.hidden) {
-      this._setUpOneOffButtons();
-      return;
-    }
     this._pendingOneOffRefresh = true;
   },
 
   // The selectedIndex is the index of the element with the "selected" class in
   // the list obtained by concatenating the suggestion rows, one-off buttons, and
   // search settings button.
   get selectedIndex() {
     let allElts = [...this._suggestionsList.children,
@@ -122,34 +118,64 @@ ContentSearchUIController.prototype = {
   set selectedIndex(idx) {
     // Update the table's rows, and the input when there is a selection.
     this._table.removeAttribute("aria-activedescendant");
     this.input.removeAttribute("aria-activedescendant");
 
     let allElts = [...this._suggestionsList.children,
                    ...this._oneOffButtons,
                    document.getElementById("contentSearchSettingsButton")];
+    // If we are selecting a suggestion and a one-off is selected, don't deselect it.
+    let excludeIndex = idx < this.numSuggestions && this.selectedButtonIndex > -1 ?
+                       this.numSuggestions + this.selectedButtonIndex : -1;
     for (let i = 0; i < allElts.length; ++i) {
       let elt = allElts[i];
       let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
       if (i == idx) {
         elt.classList.add("selected");
         ariaSelectedElt.setAttribute("aria-selected", "true");
         this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
       }
-      else {
+      else if (i != excludeIndex) {
         elt.classList.remove("selected");
         ariaSelectedElt.setAttribute("aria-selected", "false");
       }
     }
   },
 
+  get selectedButtonIndex() {
+    let elts = [...this._oneOffButtons,
+                document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < elts.length; ++i) {
+      if (elts[i].classList.contains("selected")) {
+        return i;
+      }
+    }
+    return -1;
+  },
+
+  set selectedButtonIndex(idx) {
+    let elts = [...this._oneOffButtons,
+                document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < elts.length; ++i) {
+      let elt = elts[i];
+      if (i == idx) {
+        elt.classList.add("selected");
+        elt.setAttribute("aria-selected", "true");
+      }
+      else {
+        elt.classList.remove("selected");
+        elt.setAttribute("aria-selected", "false");
+      }
+    }
+  },
+
   get selectedEngineName() {
-    let selectedElt = this._table.querySelector(".selected");
-    if (selectedElt && selectedElt.engineName) {
+    let selectedElt = this._oneOffsTable.querySelector(".selected");
+    if (selectedElt) {
       return selectedElt.engineName;
     }
     return this.defaultEngine.name;
   },
 
   get numSuggestions() {
     return this._suggestionsList.children.length;
   },
@@ -189,17 +215,17 @@ ContentSearchUIController.prototype = {
     this._sendMsg("AddFormHistoryEntry", this.input.value);
   },
 
   handleEvent: function (event) {
     this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
   },
 
   _onCommand: function(aEvent) {
-    if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
+    if (this.selectedButtonIndex == this._oneOffButtons.length) {
       // Settings button was selected.
       this._sendMsg("ManageEngines");
       return;
     }
 
     this.search(aEvent);
 
     if (aEvent) {
@@ -259,29 +285,68 @@ ContentSearchUIController.prototype = {
       this._getSuggestions();
       this.selectAndUpdateInput(-1);
     }
     this._updateSearchWithHeader();
   },
 
   _onKeypress: function (event) {
     let selectedIndexDelta = 0;
+    let selectedSuggestionDelta = 0;
+    let selectedOneOffDelta = 0;
+
     switch (event.keyCode) {
     case event.DOM_VK_UP:
-      if (!this._table.hidden) {
-        selectedIndexDelta = -1;
+      if (this._table.hidden) {
+        return;
       }
+      if (event.getModifierState("Accel")) {
+        if (event.shiftKey) {
+          selectedSuggestionDelta = -1;
+          break;
+        }
+        this._cycleCurrentEngine(true);
+        break;
+      }
+      if (event.altKey) {
+        selectedOneOffDelta = -1;
+        break;
+      }
+      selectedIndexDelta = -1;
       break;
     case event.DOM_VK_DOWN:
       if (this._table.hidden) {
         this._getSuggestions();
+        return;
       }
-      else {
-        selectedIndexDelta = 1;
+      if (event.getModifierState("Accel")) {
+        if (event.shiftKey) {
+          selectedSuggestionDelta = 1;
+          break;
+        }
+        this._cycleCurrentEngine(false);
+        break;
+      }
+      if (event.altKey) {
+        selectedOneOffDelta = 1;
+        break;
       }
+      selectedIndexDelta = 1;
+      break;
+    case event.DOM_VK_TAB:
+      if (this._table.hidden) {
+        return;
+      }
+      // Shift+tab when either the first or no one-off is selected, as well as
+      // tab when the settings button is selected, should change focus as normal.
+      if ((this.selectedButtonIndex <= 0 && event.shiftKey) ||
+          this.selectedButtonIndex == this._oneOffButtons.length && !event.shiftKey) {
+        return;
+      }
+      selectedOneOffDelta = event.shiftKey ? -1 : 1;
       break;
     case event.DOM_VK_RIGHT:
       // Allow normal caret movement until the caret is at the end of the input.
       if (this.input.selectionStart != this.input.selectionEnd ||
           this.input.selectionEnd != this.input.value.length) {
         return;
       }
       if (this.numSuggestions && this.selectedIndex >= 0 &&
@@ -292,47 +357,107 @@ ContentSearchUIController.prototype = {
       } else {
         // If we didn't select anything, make sure to remove the attributes
         // in case they were populated last time.
         this.input.removeAttribute("selection-index");
         this.input.removeAttribute("selection-kind");
       }
       this._stickyInputValue = this.input.value;
       this._hideSuggestions();
-      break;
+      return;
     case event.DOM_VK_RETURN:
       this._onCommand(event);
-      break;
+      return;
     case event.DOM_VK_DELETE:
       if (this.selectedIndex >= 0) {
         this.deleteSuggestionAtIndex(this.selectedIndex);
       }
-      break;
+      return;
     case event.DOM_VK_ESCAPE:
       if (!this._table.hidden) {
         this._hideSuggestions();
       }
+      return;
     default:
       return;
     }
 
+    let currentIndex = this.selectedIndex;
     if (selectedIndexDelta) {
-      // Update the selection.
-      let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
+      let newSelectedIndex = currentIndex + selectedIndexDelta;
       if (newSelectedIndex < -1) {
         newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
       }
-      else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
+      // If are moving up from the first one off, we have to deselect the one off
+      // manually because the selectedIndex setter tries to exclude the selected
+      // one-off (which is desirable for accel+shift+up/down).
+      if (currentIndex == this.numSuggestions && selectedIndexDelta == -1) {
+        this.selectedButtonIndex = -1;
+      }
+      this.selectAndUpdateInput(newSelectedIndex);
+    }
+
+    else if (selectedSuggestionDelta) {
+      let newSelectedIndex;
+      if (currentIndex >= this.numSuggestions || currentIndex == -1) {
+        // No suggestion already selected, select the first/last one appropriately.
+        newSelectedIndex = selectedSuggestionDelta == 1 ?
+                           0 : this.numSuggestions - 1;
+      }
+      else {
+        newSelectedIndex = currentIndex + selectedSuggestionDelta;
+      }
+      if (newSelectedIndex >= this.numSuggestions) {
         newSelectedIndex = -1;
       }
       this.selectAndUpdateInput(newSelectedIndex);
+    }
 
-      // Prevent the input's caret from moving.
-      event.preventDefault();
+    else if (selectedOneOffDelta) {
+      let newSelectedIndex;
+      let currentButton = this.selectedButtonIndex;
+      if (currentButton == -1 || currentButton == this._oneOffButtons.length) {
+        // No one-off already selected, select the first/last one appropriately.
+        newSelectedIndex = selectedOneOffDelta == 1 ?
+                           0 : this._oneOffButtons.length - 1;
+      }
+      else {
+        newSelectedIndex = currentButton + selectedOneOffDelta;
+      }
+      // Allow selection of the settings button via the tab key.
+      if (newSelectedIndex == this._oneOffButtons.length &&
+          event.keyCode != event.DOM_VK_TAB) {
+        newSelectedIndex = -1;
+      }
+      this.selectedButtonIndex = newSelectedIndex;
     }
+
+    // Prevent the input's caret from moving.
+    event.preventDefault();
+  },
+
+  _currentEngineIndex: -1,
+  _cycleCurrentEngine: function (aReverse) {
+    if ((this._currentEngineIndex == this._oneOffButtons.length - 1 && !aReverse) ||
+        (this._currentEngineIndex < 0 && aReverse)) {
+      return;
+    }
+    this._currentEngineIndex += aReverse ? -1 : 1;
+    let engine;
+    if (this._currentEngineIndex == -1) {
+      engine = this._originalDefaultEngine;
+    } else {
+      let button = this._oneOffButtons[this._currentEngineIndex];
+      engine = {
+        name: button.engineName,
+        icon: button.firstChild.getAttribute("src"),
+      };
+    }
+    this._sendMsg("SetCurrentEngine", engine.name);
+    this.defaultEngine = engine;
   },
 
   _onFocus: function () {
     if (this._mousedown) {
       return;
     }
     // When the input box loses focus to something in our table, we refocus it
     // immediately. This causes the focus highlight to flicker, so we set a
@@ -351,26 +476,40 @@ ContentSearchUIController.prototype = {
       setTimeout(() => this.input.focus(), 0);
       return;
     }
     this.input.removeAttribute("keepfocus");
     this._hideSuggestions();
   },
 
   _onMousemove: function (event) {
-    this.selectedIndex = this._indexOfTableItem(event.target);
+    let idx = this._indexOfTableItem(event.target);
+    if (idx >= this.numSuggestions) {
+      this.selectedButtonIndex = idx - this.numSuggestions;
+      return;
+    }
+    this.selectedIndex = idx;
   },
 
   _onMouseup: function (event) {
     if (event.button == 2) {
       return;
     }
     this._onCommand(event);
   },
 
+  _onMouseout: function (event) {
+    // We only deselect one-off buttons and the settings button when they are
+    // moused out.
+    let idx = this._indexOfTableItem(event.originalTarget);
+    if (idx >= this.numSuggestions) {
+      this.selectedButtonIndex = -1;
+    }
+  },
+
   _onClick: function (event) {
     this._onMouseup(event);
   },
 
   _onContentSearchService: function (event) {
     let methodName = "_onMsg" + event.detail.type;
     if (methodName in this) {
       this[methodName](event.detail.data);
@@ -422,16 +561,20 @@ ContentSearchUIController.prototype = {
     if (this._table.hidden) {
       this.selectedIndex = -1;
       if (this._pendingOneOffRefresh) {
         this._setUpOneOffButtons();
         delete this._pendingOneOffRefresh;
       }
       this._table.hidden = false;
       this.input.setAttribute("aria-expanded", "true");
+      this._originalDefaultEngine = {
+        name: this.defaultEngine.name,
+        icon: this.defaultEngine.icon,
+      };
     }
   },
 
   _onMsgState: function (state) {
     this.defaultEngine = {
       name: state.currentEngine.name,
       icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
     };
@@ -442,20 +585,16 @@ ContentSearchUIController.prototype = {
     this._onMsgState(state);
   },
 
   _onMsgCurrentEngine: function (engine) {
     this.defaultEngine = {
       name: engine.name,
       icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
     };
-    if (!this._table.hidden) {
-      this._setUpOneOffButtons();
-      return;
-    }
     this._pendingOneOffRefresh = true;
   },
 
   _onMsgStrings: function (strings) {
     this._strings = strings;
     this._updateDefaultEngineHeader();
     this._updateSearchWithHeader();
     document.getElementById("contentSearchSettingsButton").textContent =
@@ -567,16 +706,19 @@ ContentSearchUIController.prototype = {
   _clearSuggestionRows: function() {
     while (this._suggestionsList.firstElementChild) {
       this._suggestionsList.firstElementChild.remove();
     }
   },
 
   _hideSuggestions: function () {
     this.input.setAttribute("aria-expanded", "false");
+    this.selectedIndex = -1;
+    this.selectedButtonIndex = -1;
+    this._currentEngineIndex = -1;
     this._table.hidden = true;
   },
 
   _indexOfTableItem: function (elt) {
     if (elt.classList.contains("contentSearchOneOffItem")) {
       return this.numSuggestions + this._oneOffButtons.indexOf(elt);
     }
     if (elt.classList.contains("contentSearchSettingsButton")) {
@@ -600,21 +742,17 @@ ContentSearchUIController.prototype = {
 
     // When the search input box loses focus, we want to immediately give focus
     // back to it if the blur was because the user clicked somewhere in the table.
     // onBlur uses the _mousedown flag to detect this.
     this._table.addEventListener("mousedown", () => { this._mousedown = true; });
     document.addEventListener("mouseup", () => { delete this._mousedown; });
 
     // Deselect the selected element on mouseout if it wasn't a suggestion.
-    this._table.addEventListener("mouseout", () => {
-      if (this.selectedIndex >= this.numSuggestions) {
-        this.selectAndUpdateInput(-1);
-      }
-    });
+    this._table.addEventListener("mouseout", this);
 
     // If a search is loaded in the same tab, ensure the suggestions dropdown
     // is hidden immediately when the page starts loading and not when it first
     // appears, in order to provide timely feedback to the user.
     window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
 
     let headerRow = document.createElementNS(HTML_NS, "tr");
     let header = document.createElementNS(HTML_NS, "td");
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1046,17 +1046,18 @@ nsContextMenu.prototype = {
     urlSecurityCheck(this.imageDescURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                                        referrerURI: gContextMenuContentData.documentURIObject });
   },
 
   viewFrameInfo: function() {
-    BrowserPageInfo(this.target.ownerDocument);
+    BrowserPageInfo(this.target.ownerDocument, null, null,
+                    this.frameOuterWindowID);
   },
 
   reloadImage: function() {
     urlSecurityCheck(this.mediaURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
 
     this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
--- a/browser/base/content/pageinfo/feeds.js
+++ b/browser/base/content/pageinfo/feeds.js
@@ -1,48 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
-  "resource:///modules/Feeds.jsm");
-
-function initFeedTab()
+function initFeedTab(feeds)
 {
-  const feedTypes = {
-    "application/rss+xml": gBundle.getString("feedRss"),
-    "application/atom+xml": gBundle.getString("feedAtom"),
-    "text/xml": gBundle.getString("feedXML"),
-    "application/xml": gBundle.getString("feedXML"),
-    "application/rdf+xml": gBundle.getString("feedXML")
-  };
-
-  // get the feeds
-  var linkNodes = gDocument.getElementsByTagName("link");
-  var length = linkNodes.length;
-  for (var i = 0; i < length; i++) {
-    var link = linkNodes[i];
-    if (!link.href)
-      continue;
-
-    var rel = link.rel && link.rel.toLowerCase();
-    var rels = {};
-    if (rel) {
-      for each (let relVal in rel.split(/\s+/))
-        rels[relVal] = true;
-    }
-
-    if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) {
-      var type = Feeds.isValidFeed(link, gDocument.nodePrincipal, "feed" in rels);
-      if (type) {
-        type = feedTypes[type] || feedTypes["application/rss+xml"];
-        addRow(link.title, type, link.href);
-      }
-    }
+  for (let feed of feeds) {
+    let [name, type, url] = feed;
+    addRow(name, type, url);
   }
 
   var feedListbox = document.getElementById("feedListbox");
   document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0;
 }
 
 function onSubscribeFeed()
 {
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -47,18 +47,29 @@ pageInfoTreeView.prototype = {
   {
     this.data[row][column.index] = value;
   },
 
   addRow: function(row)
   {
     this.rows = this.data.push(row);
     this.rowCountChanged(this.rows - 1, 1);
-    if (this.selection.count == 0 && this.rowCount && !gImageElement)
+    if (this.selection.count == 0 && this.rowCount && !gImageElement) {
       this.selection.select(0);
+    }
+  },
+
+  addRows: function(rows)
+  {
+    this.data = this.data.concat(rows);
+    this.rowCountChanged(this.rows, rows.length);
+    this.rows = this.data.length;
+    if (this.selection.count == 0 && this.rowCount && !gImageElement) {
+      this.selection.select(0);
+    }
   },
 
   rowCountChanged: function(index, count)
   {
     this.tree.rowCountChanged(index, count);
   },
 
   invalidate: function()
@@ -135,18 +146,17 @@ pageInfoTreeView.prototype = {
   cycleCell: function(row, column) { },
   isEditable: function(row, column) { return false; },
   isSelectable: function(row, column) { return false; },
   performAction: function(action) { },
   performActionOnCell: function(action, row, column) { }
 };
 
 // mmm, yummy. global variables.
-var gWindow = null;
-var gDocument = null;
+var gDocInfo = null;
 var gImageElement = null;
 
 // column number to help using the data array
 const COL_IMAGE_ADDRESS = 0;
 const COL_IMAGE_TYPE    = 1;
 const COL_IMAGE_SIZE    = 2;
 const COL_IMAGE_ALT     = 3;
 const COL_IMAGE_COUNT   = 4;
@@ -281,44 +291,32 @@ const XHTMLre = RegExp(XHTMLNSre + "|" +
  * These arrays are used to hold callbacks that Page Info will call at
  * various stages. Use them by simply appending a function to them.
  * For example, add a function to onLoadRegistry by invoking
  *   "onLoadRegistry.push(XXXLoadFunc);"
  * The XXXLoadFunc should be unique to the overlay module, and will be
  * invoked as "XXXLoadFunc();"
  */
 
-// These functions are called to build the data displayed in the Page
-// Info window. The global variables gDocument and gWindow are set.
+// These functions are called to build the data displayed in the Page Info window.
 var onLoadRegistry = [ ];
 
 // These functions are called to remove old data still displayed in
 // the window when the document whose information is displayed
 // changes. For example, at this time, the list of images of the Media
 // tab is cleared.
 var onResetRegistry = [ ];
 
-// These are called once for each subframe of the target document and
-// the target document itself. The frame is passed as an argument.
-var onProcessFrame = [ ];
-
-// These functions are called once for each element (in all subframes, if any)
-// in the target document. The element is passed as an argument.
-var onProcessElement = [ ];
-
 // These functions are called once when all the elements in all of the target
 // document (and all of its subframes, if any) have been processed
 var onFinished = [ ];
 
 // These functions are called once when the Page Info window is closed.
 var onUnloadRegistry = [ ];
 
-// These functions are called once when an image preview is shown.
-var onImagePreviewShown = [ ];
-
 /* Called when PageInfo window is loaded.  Arguments are:
  *  window.arguments[0] - (optional) an object consisting of
  *                         - doc: (optional) document to use for source. if not provided,
  *                                the calling window's document will be used
  *                         - initialTab: (optional) id of the inital tab to display
  */
 function onLoadPageInfo()
 {
@@ -336,48 +334,73 @@ function onLoadPageInfo()
   gStrings.mediaInput = gBundle.getString("mediaInput");
   gStrings.mediaVideo = gBundle.getString("mediaVideo");
   gStrings.mediaAudio = gBundle.getString("mediaAudio");
 
   var args = "arguments" in window &&
              window.arguments.length >= 1 &&
              window.arguments[0];
 
-  if (!args || !args.doc) {
-    gWindow = window.opener.gBrowser.selectedBrowser.contentWindowAsCPOW;
-    gDocument = gWindow.document;
-  }
-
   // init media view
   var imageTree = document.getElementById("imagetree");
   imageTree.view = gImageView;
 
   /* Select the requested tab, if the name is specified */
   loadTab(args);
   Components.classes["@mozilla.org/observer-service;1"]
             .getService(Components.interfaces.nsIObserverService)
             .notifyObservers(window, "page-info-dialog-loaded", null);
 }
 
-function loadPageInfo()
+function loadPageInfo(frameOuterWindowID)
 {
-  var titleFormat = gWindow != gWindow.top ? "pageInfo.frame.title"
-                                           : "pageInfo.page.title";
-  document.title = gBundle.getFormattedString(titleFormat, [gDocument.location]);
+  let mm = window.opener.gBrowser.selectedBrowser.messageManager;
 
-  document.getElementById("main-window").setAttribute("relatedUrl", gDocument.location);
+  gStrings["application/rss+xml"]  = gBundle.getString("feedRss");
+  gStrings["application/atom+xml"] = gBundle.getString("feedAtom");
+  gStrings["text/xml"]             = gBundle.getString("feedXML");
+  gStrings["application/xml"]      = gBundle.getString("feedXML");
+  gStrings["application/rdf+xml"]  = gBundle.getString("feedXML");
+
+  // Look for pageInfoListener in content.js. Sends message to listener with arguments.
+  mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings,
+                      frameOuterWindowID: frameOuterWindowID});
+
+  let pageInfoData = null;
 
-  // do the easy stuff first
-  makeGeneralTab();
+  // Get initial pageInfoData needed to display the general, feeds, permission and security tabs.
+  mm.addMessageListener("PageInfo:data", function onmessage(message) {
+    mm.removeMessageListener("PageInfo:data", onmessage);
+    pageInfoData = message.data;
+    let docInfo = pageInfoData.docInfo;
+    let windowInfo = pageInfoData.windowInfo;
+    let uri = makeURI(docInfo.documentURIObject.spec,
+                      docInfo.documentURIObject.originCharset);
+    gDocInfo = docInfo;
+
+    var titleFormat = windowInfo.isTopWindow ? "pageInfo.frame.title"
+                                             : "pageInfo.page.title";
+    document.title = gBundle.getFormattedString(titleFormat, [docInfo.location]);
 
-  // and then the hard stuff
-  makeTabs(gDocument, gWindow);
+    document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location);
+
+    makeGeneralTab(pageInfoData.metaViewRows, docInfo);
+    initFeedTab(pageInfoData.feeds);
+    onLoadPermission(uri);
+    securityOnLoad(uri, windowInfo);
+  });
 
-  initFeedTab();
-  onLoadPermission();
+  // Get the media elements from content script to setup the media tab.
+  mm.addMessageListener("PageInfo:mediaData", function onmessage(message){
+    mm.removeMessageListener("PageInfo:mediaData", onmessage);
+    makeMediaTab(message.data.imageViewRows);
+
+    // Loop through onFinished and execute the functions on it.
+    onFinished.forEach(function(func) { func(pageInfoData); });
+  });
 
   /* Call registered overlay init functions */
   onLoadRegistry.forEach(function(func) { func(); });
 }
 
 function resetPageInfo(args)
 {
   /* Reset Meta tags part */
@@ -438,25 +461,32 @@ function showTab(id)
 {
   var deck  = document.getElementById("mainDeck");
   var pagel = document.getElementById(id + "Panel");
   deck.selectedPanel = pagel;
 }
 
 function loadTab(args)
 {
-  if (args && args.doc) {
-    gDocument = args.doc;
-    gWindow = gDocument.defaultView;
+  // If the "View Image Info" context menu item was used, the related image
+  // element is provided as an argument. This can't be a background image.
+  let imageElement = args && args.imageElement;
+  if (imageElement) {
+    gImageElement = {currentSrc: imageElement.currentSrc,
+                     width: imageElement.width, height: imageElement.height,
+                     imageText: imageElement.title || imageElement.alt};
+  }
+  else {
+    gImageElement = null;
   }
 
-  gImageElement = args && args.imageElement;
+  let frameOuterWindowID = args && args.frameOuterWindowID;
 
   /* Load the page info */
-  loadPageInfo();
+  loadPageInfo(frameOuterWindowID);
 
   var initialTab = (args && args.initialTab) || "generalTab";
   var radioGroup = document.getElementById("viewGroup");
   initialTab = document.getElementById(initialTab) || document.getElementById("generalTab");
   radioGroup.selectedItem = initialTab;
   radioGroup.selectedItem.doCommand();
   radioGroup.focus();
 }
@@ -486,127 +516,83 @@ function openCacheEntry(key, cb)
     },
     onCacheEntryAvailable: function(entry, isNew, appCache, status) {
       cb(entry);
     }
   };
   diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", nsICacheStorage.OPEN_READONLY, checkCacheListener);
 }
 
-function makeGeneralTab()
+function makeGeneralTab(metaViewRows, docInfo)
 {
-  var title = (gDocument.title) ? gBundle.getFormattedString("pageTitle", [gDocument.title]) : gBundle.getString("noPageTitle");
+  var title = (docInfo.title) ? gBundle.getFormattedString("pageTitle", [docInfo.title]) : gBundle.getString("noPageTitle");
   document.getElementById("titletext").value = title;
 
-  var url = gDocument.location.toString();
+  var url = docInfo.location;
   setItemValue("urltext", url);
 
-  var referrer = ("referrer" in gDocument && gDocument.referrer);
+  var referrer = ("referrer" in docInfo && docInfo.referrer);
   setItemValue("refertext", referrer);
 
-  var mode = ("compatMode" in gDocument && gDocument.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode";
+  var mode = ("compatMode" in docInfo && docInfo.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode";
   document.getElementById("modetext").value = gBundle.getString(mode);
 
   // find out the mime type
-  var mimeType = gDocument.contentType;
+  var mimeType = docInfo.contentType;
   setItemValue("typetext", mimeType);
 
   // get the document characterset
-  var encoding = gDocument.characterSet;
+  var encoding = docInfo.characterSet;
   document.getElementById("encodingtext").value = encoding;
 
-  // get the meta tags
-  var metaNodes = gDocument.getElementsByTagName("meta");
-  var length = metaNodes.length;
+  let length = metaViewRows.length;
 
   var metaGroup = document.getElementById("metaTags");
   if (!length)
     metaGroup.collapsed = true;
   else {
     var metaTagsCaption = document.getElementById("metaTagsCaption");
     if (length == 1)
       metaTagsCaption.label = gBundle.getString("generalMetaTag");
     else
       metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]);
     var metaTree = document.getElementById("metatree");
     metaTree.view = gMetaView;
 
-    for (var i = 0; i < length; i++)
-      gMetaView.addRow([metaNodes[i].name || metaNodes[i].httpEquiv || metaNodes[i].getAttribute("property"),
-                        metaNodes[i].content]);
+    // Add the metaViewRows onto the general tab's meta info tree.
+    gMetaView.addRows(metaViewRows);
 
     metaGroup.collapsed = false;
   }
 
   // get the date of last modification
-  var modifiedText = formatDate(gDocument.lastModified, gStrings.notSet);
+  var modifiedText = formatDate(docInfo.lastModified, gStrings.notSet);
   document.getElementById("modifiedtext").value = modifiedText;
 
   // get cache info
   var cacheKey = url.replace(/#.*$/, "");
   openCacheEntry(cacheKey, function(cacheEntry) {
     var sizeText;
     if (cacheEntry) {
       var pageSize = cacheEntry.dataSize;
       var kbSize = formatNumber(Math.round(pageSize / 1024 * 100) / 100);
       sizeText = gBundle.getFormattedString("generalSize", [kbSize, formatNumber(pageSize)]);
     }
     setItemValue("sizetext", sizeText);
   });
-
-  securityOnLoad();
-}
-
-//******** Generic Build-a-tab
-// Assumes the views are empty. Only called once to build the tabs, and
-// does so by farming the task off to another thread via setTimeout().
-// The actual work is done with a TreeWalker that calls doGrab() once for
-// each element node in the document.
-
-var gFrameList = [ ];
-
-function makeTabs(aDocument, aWindow)
-{
-  goThroughFrames(aDocument, aWindow);
-  processFrames();
 }
 
-function goThroughFrames(aDocument, aWindow)
-{
-  gFrameList.push(aDocument);
-  if (aWindow && aWindow.frames.length > 0) {
-    var num = aWindow.frames.length;
-    for (var i = 0; i < num; i++)
-      goThroughFrames(aWindow.frames[i].document, aWindow.frames[i]);  // recurse through the frames
-  }
-}
-
-function processFrames()
+function makeMediaTab(imageViewRows)
 {
-  if (gFrameList.length) {
-    var doc = gFrameList[0];
-    onProcessFrame.forEach(function(func) { func(doc); });
-    var iterator = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, grabAll);
-    gFrameList.shift();
-    setTimeout(doGrab, 10, iterator);
-    onFinished.push(selectImage);
+  // Call addImage passing in the image rows to add to the view on the Media Tab.
+  for (let image of imageViewRows) {
+    let [url, type, alt, elem, isBg] = image;
+    addImage(url, type, alt, elem, isBg);
   }
-  else
-    onFinished.forEach(function(func) { func(); });
-}
-
-function doGrab(iterator)
-{
-  for (var i = 0; i < 500; ++i)
-    if (!iterator.nextNode()) {
-      processFrames();
-      return;
-    }
-
-  setTimeout(doGrab, 10, iterator);
+  selectImage();
 }
 
 function addImage(url, type, alt, elem, isBg)
 {
   if (!url)
     return;
 
   if (!gImageHash.hasOwnProperty(url))
@@ -634,88 +620,27 @@ function addImage(url, type, alt, elem, 
       Components.classes["@mozilla.org/observer-service;1"]
                 .getService(Components.interfaces.nsIObserverService)
                 .addObserver(imagePermissionObserver, "perm-changed", false);
     }
   }
   else {
     var i = gImageHash[url][type][alt];
     gImageView.data[i][COL_IMAGE_COUNT]++;
-    if (elem == gImageElement)
+    // The same image can occur several times on the page at different sizes.
+    // If the "View Image Info" context menu item was used, ensure we select
+    // the correct element.
+    if (!gImageView.data[i][COL_IMAGE_BG] &&
+        gImageElement && url == gImageElement.currentSrc &&
+        gImageElement.width == elem.width &&
+        gImageElement.height == elem.height &&
+        gImageElement.imageText == elem.imageText) {
       gImageView.data[i][COL_IMAGE_NODE] = elem;
-  }
-}
-
-function grabAll(elem)
-{
-  // check for images defined in CSS (e.g. background, borders), any node may have multiple
-  var computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, "");
-
-  if (computedStyle) {
-    var addImgFunc = function (label, val) {
-      if (val.primitiveType == CSSPrimitiveValue.CSS_URI) {
-        addImage(val.getStringValue(), label, gStrings.notSet, elem, true);
-      }
-      else if (val.primitiveType == CSSPrimitiveValue.CSS_STRING) {
-        // This is for -moz-image-rect.
-        // TODO: Reimplement once bug 714757 is fixed
-        var strVal = val.getStringValue();
-        if (strVal.search(/^.*url\(\"?/) > -1) {
-          url = strVal.replace(/^.*url\(\"?/,"").replace(/\"?\).*$/,"");
-          addImage(url, label, gStrings.notSet, elem, true);
-        }
-      }
-      else if (val.cssValueType == CSSValue.CSS_VALUE_LIST) {
-        // recursively resolve multiple nested CSS value lists
-        for (var i = 0; i < val.length; i++)
-          addImgFunc(label, val.item(i));
-      }
-    };
-
-    addImgFunc(gStrings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image"));
-    addImgFunc(gStrings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source"));
-    addImgFunc(gStrings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image"));
-    addImgFunc(gStrings.mediaCursor, computedStyle.getPropertyCSSValue("cursor"));
+    }
   }
-
-  // one swi^H^H^Hif-else to rule them all
-  if (elem instanceof HTMLImageElement)
-    addImage(elem.src, gStrings.mediaImg,
-             (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false);
-  else if (elem instanceof SVGImageElement) {
-    try {
-      // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI
-      //       or the URI formed from the baseURI and the URL is not a valid URI
-      var href = makeURLAbsolute(elem.baseURI, elem.href.baseVal);
-      addImage(href, gStrings.mediaImg, "", elem, false);
-    } catch (e) { }
-  }
-  else if (elem instanceof HTMLVideoElement) {
-    addImage(elem.currentSrc, gStrings.mediaVideo, "", elem, false);
-  }
-  else if (elem instanceof HTMLAudioElement) {
-    addImage(elem.currentSrc, gStrings.mediaAudio, "", elem, false);
-  }
-  else if (elem instanceof HTMLLinkElement) {
-    if (elem.rel && /\bicon\b/i.test(elem.rel))
-      addImage(elem.href, gStrings.mediaLink, "", elem, false);
-  }
-  else if (elem instanceof HTMLInputElement || elem instanceof HTMLButtonElement) {
-    if (elem.type.toLowerCase() == "image")
-      addImage(elem.src, gStrings.mediaInput,
-               (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false);
-  }
-  else if (elem instanceof HTMLObjectElement)
-    addImage(elem.data, gStrings.mediaObject, getValueText(elem), elem, false);
-  else if (elem instanceof HTMLEmbedElement)
-    addImage(elem.src, gStrings.mediaEmbed, "", elem, false);
-
-  onProcessElement.forEach(function(func) { func(elem); });
-
-  return NodeFilter.FILTER_ACCEPT;
 }
 
 //******** Link Stuff
 function openURL(target)
 {
   var url = target.parentNode.childNodes[2].value;
   window.open(url, "_blank", "chrome");
 }
@@ -809,24 +734,25 @@ function saveMedia()
     if (url) {
       var titleKey = "SaveImageTitle";
 
       if (item instanceof HTMLVideoElement)
         titleKey = "SaveVideoTitle";
       else if (item instanceof HTMLAudioElement)
         titleKey = "SaveAudioTitle";
 
-      saveURL(url, null, titleKey, false, false, makeURI(item.baseURI), gDocument);
+      saveURL(url, null, titleKey, false, false, makeURI(item.baseURI),
+              null, gDocInfo.isContentWindowPrivate);
     }
   } else {
     selectSaveFolder(function(aDirectory) {
       if (aDirectory) {
         var saveAnImage = function(aURIString, aChosenData, aBaseURI) {
           internalSave(aURIString, null, null, null, null, false, "SaveImageTitle",
-                       aChosenData, aBaseURI, gDocument);
+                       aChosenData, aBaseURI, null, gDocInfo.isContentWindowPrivate);
         };
 
         for (var i = 0; i < rowArray.length; i++) {
           var v = rowArray[i];
           var dir = aDirectory.clone();
           var item = gImageView.data[v][COL_IMAGE_NODE];
           var uriString = gImageView.data[v][COL_IMAGE_ADDRESS];
           var uri = makeURI(uriString);
@@ -888,37 +814,27 @@ function onImageSelect()
     mediaSaveBox.collapsed = true;
     splitter.collapsed     = false;
     previewBox.collapsed   = false;
     tree.flex = 0;
     makePreview(getSelectedRows(tree)[0]);
   }
 }
 
+// Makes the media preview (image, video, etc) for the selected row on the media tab.
 function makePreview(row)
 {
   var imageTree = document.getElementById("imagetree");
   var item = gImageView.data[row][COL_IMAGE_NODE];
   var url = gImageView.data[row][COL_IMAGE_ADDRESS];
   var isBG = gImageView.data[row][COL_IMAGE_BG];
   var isAudio = false;
 
   setItemValue("imageurltext", url);
-
-  var imageText;
-  if (!isBG &&
-      !(item instanceof SVGImageElement) &&
-      !(gDocument instanceof ImageDocument)) {
-    imageText = item.title || item.alt;
-
-    if (!imageText && !(item instanceof HTMLImageElement))
-      imageText = getValueText(item);
-  }
-  setItemValue("imagetext", imageText);
-
+  setItemValue("imagetext", item.imageText);
   setItemValue("imagelongdesctext", item.longDesc);
 
   // get cache info
   var cacheKey = url.replace(/#.*$/, "");
   openCacheEntry(cacheKey, function(cacheEntry) {
     // find out the file size
     var sizeText;
     if (cacheEntry) {
@@ -926,42 +842,18 @@ function makePreview(row)
       var kbSize = Math.round(imageSize / 1024 * 100) / 100;
       sizeText = gBundle.getFormattedString("generalSize",
                                             [formatNumber(kbSize), formatNumber(imageSize)]);
     }
     else
       sizeText = gBundle.getString("mediaUnknownNotCached");
     setItemValue("imagesizetext", sizeText);
 
-    var mimeType;
-    var numFrames = 1;
-    if (item instanceof HTMLObjectElement ||
-        item instanceof HTMLEmbedElement ||
-        item instanceof HTMLLinkElement)
-      mimeType = item.type;
-
-    if (!mimeType && !isBG && item instanceof nsIImageLoadingContent) {
-      var imageRequest = item.getRequest(nsIImageLoadingContent.CURRENT_REQUEST);
-      if (imageRequest) {
-        mimeType = imageRequest.mimeType;
-        var image = imageRequest.image;
-        if (image)
-          numFrames = image.numFrames;
-      }
-    }
-
-    if (!mimeType)
-      mimeType = getContentTypeFromHeaders(cacheEntry);
-
-    // if we have a data url, get the MIME type from the url
-    if (!mimeType && url.startsWith("data:")) {
-      let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url);
-      if (dataMimeType)
-        mimeType = dataMimeType[1].toLowerCase();
-    }
+    var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry);
+    var numFrames = item.numFrames;
 
     var imageType;
     if (mimeType) {
       // We found the type, try to display it nicely
       let imageMimeType = /^image\/(.*)/i.exec(mimeType);
       if (imageMimeType) {
         imageType = imageMimeType[1].toUpperCase();
         if (numFrames > 1)
@@ -986,20 +878,20 @@ function makePreview(row)
 
     var isProtocolAllowed = checkProtocol(gImageView.data[row]);
 
     var newImage = new Image;
     newImage.id = "thepreviewimage";
     var physWidth = 0, physHeight = 0;
     var width = 0, height = 0;
 
-    if ((item instanceof HTMLLinkElement || item instanceof HTMLInputElement ||
-         item instanceof HTMLImageElement ||
-         item instanceof SVGImageElement ||
-         (item instanceof HTMLObjectElement && mimeType && mimeType.startsWith("image/")) || isBG) && isProtocolAllowed) {
+    if ((item.HTMLLinkElement || item.HTMLInputElement ||
+         item.HTMLImageElement || item.SVGImageElement ||
+         (item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) ||
+         isBG) && isProtocolAllowed) {
       newImage.setAttribute("src", url);
       physWidth = newImage.width || 0;
       physHeight = newImage.height || 0;
 
       // "width" and "height" attributes must be set to newImage,
       // even if there is no "width" or "height attribute in item;
       // otherwise, the preview image cannot be displayed correctly.
       if (!isBG) {
@@ -1008,39 +900,39 @@ function makePreview(row)
       }
       else {
         // the Width and Height of an HTML tag should not be used for its background image
         // (for example, "table" can have "width" or "height" attributes)
         newImage.width = newImage.naturalWidth;
         newImage.height = newImage.naturalHeight;
       }
 
-      if (item instanceof SVGImageElement) {
-        newImage.width = item.width.baseVal.value;
-        newImage.height = item.height.baseVal.value;
+      if (item.SVGImageElement) {
+        newImage.width = item.SVGImageElementWidth;
+        newImage.height = item.SVGImageElementHeight;
       }
 
       width = newImage.width;
       height = newImage.height;
 
       document.getElementById("theimagecontainer").collapsed = false
       document.getElementById("brokenimagecontainer").collapsed = true;
     }
-    else if (item instanceof HTMLVideoElement && isProtocolAllowed) {
+    else if (item.HTMLVideoElement && isProtocolAllowed) {
       newImage = document.createElementNS("http://www.w3.org/1999/xhtml", "video");
       newImage.id = "thepreviewimage";
       newImage.src = url;
       newImage.controls = true;
       width = physWidth = item.videoWidth;
       height = physHeight = item.videoHeight;
 
       document.getElementById("theimagecontainer").collapsed = false;
       document.getElementById("brokenimagecontainer").collapsed = true;
     }
-    else if (item instanceof HTMLAudioElement && isProtocolAllowed) {
+    else if (item.HTMLAudioElement && isProtocolAllowed) {
       newImage = new Audio;
       newImage.id = "thepreviewimage";
       newImage.src = url;
       newImage.controls = true;
       isAudio = true;
 
       document.getElementById("theimagecontainer").collapsed = false;
       document.getElementById("brokenimagecontainer").collapsed = true;
@@ -1068,18 +960,16 @@ function makePreview(row)
       }
     }
     setItemValue("imagedimensiontext", imageSize);
 
     makeBlockImage(url);
 
     imageContainer.removeChild(oldImage);
     imageContainer.appendChild(newImage);
-
-    onImagePreviewShown.forEach(function(func) { func(); });
   });
 }
 
 function makeBlockImage(url)
 {
   var permissionManager = Components.classes[PERMISSION_CONTRACTID]
                                     .getService(nsIPermissionManager);
   var prefs = Components.classes[PREFERENCES_CONTRACTID]
@@ -1125,79 +1015,19 @@ var imagePermissionObserver = {
   }
 }
 
 function getContentTypeFromHeaders(cacheEntryDescriptor)
 {
   if (!cacheEntryDescriptor)
     return null;
 
-  return (/^Content-Type:\s*(.*?)\s*(?:\;|$)/mi
-          .exec(cacheEntryDescriptor.getMetaDataElement("response-head")))[1];
-}
-
-//******** Other Misc Stuff
-// Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
-// parse a node to extract the contents of the node
-function getValueText(node)
-{
-  var valueText = "";
-
-  // form input elements don't generally contain information that is useful to our callers, so return nothing
-  if (node instanceof HTMLInputElement ||
-      node instanceof HTMLSelectElement ||
-      node instanceof HTMLTextAreaElement)
-    return valueText;
-
-  // otherwise recurse for each child
-  var length = node.childNodes.length;
-  for (var i = 0; i < length; i++) {
-    var childNode = node.childNodes[i];
-    var nodeType = childNode.nodeType;
-
-    // text nodes are where the goods are
-    if (nodeType == Node.TEXT_NODE)
-      valueText += " " + childNode.nodeValue;
-    // and elements can have more text inside them
-    else if (nodeType == Node.ELEMENT_NODE) {
-      // images are special, we want to capture the alt text as if the image weren't there
-      if (childNode instanceof HTMLImageElement)
-        valueText += " " + getAltText(childNode);
-      else
-        valueText += " " + getValueText(childNode);
-    }
-  }
-
-  return stripWS(valueText);
-}
-
-// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
-// traverse the tree in search of an img or area element and grab its alt tag
-function getAltText(node)
-{
-  var altText = "";
-
-  if (node.alt)
-    return node.alt;
-  var length = node.childNodes.length;
-  for (var i = 0; i < length; i++)
-    if ((altText = getAltText(node.childNodes[i]) != undefined))  // stupid js warning...
-      return altText;
-  return "";
-}
-
-// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
-// strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space
-function stripWS(text)
-{
-  var middleRE = /\s+/g;
-  var endRE = /(^\s+)|(\s+$)/g;
-
-  text = text.replace(middleRE, " ");
-  return text.replace(endRE, "");
+  let headers = cacheEntryDescriptor.getMetaDataElement("response-head");
+  let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/mi.exec(headers);
+  return type && type[1];
 }
 
 function setItemValue(id, value)
 {
   var item = document.getElementById(id);
   if (value) {
     item.parentNode.collapsed = false;
     item.value = value;
@@ -1276,18 +1106,23 @@ function doSelectAll()
 
 function selectImage()
 {
   if (!gImageElement)
     return;
 
   var tree = document.getElementById("imagetree");
   for (var i = 0; i < tree.view.rowCount; i++) {
-    if (gImageElement == gImageView.data[i][COL_IMAGE_NODE] &&
-        !gImageView.data[i][COL_IMAGE_BG]) {
+    // If the image row element is the image selected from the "View Image Info" context menu item.
+    let image = gImageView.data[i][COL_IMAGE_NODE];
+    if (!gImageView.data[i][COL_IMAGE_BG] &&
+        gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] &&
+        gImageElement.width == image.width &&
+        gImageElement.height == image.height &&
+        gImageElement.imageText == image.imageText) {
       tree.view.selection.select(i);
       tree.treeBoxObject.ensureRowIsVisible(i);
       tree.focus();
       return;
     }
   }
 }
 
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -23,19 +23,18 @@ var permissionObserver = {
           initRow(permission.type);
         else if (permission.type.startsWith("plugin"))
           setPluginsRadioState();
       }
     }
   }
 };
 
-function onLoadPermission()
+function onLoadPermission(uri)
 {
-  var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
   var permTab = document.getElementById("permTab");
   if (SitePermissions.isSupportedURI(uri)) {
     gPermURI = uri;
     var hostText = document.getElementById("hostText");
     hostText.value = gPermURI.prePath;
 
     for (var i of gPermissions)
       initRow(i);
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -4,39 +4,40 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 
 var security = {
+  init: function(uri, windowInfo) {
+    this.uri = uri;
+    this.windowInfo = windowInfo;
+  },
+
   // Display the server certificate (static)
   viewCert : function () {
     var cert = security._cert;
     viewCertHelper(window, cert);
   },
 
   _getSecurityInfo : function() {
     const nsIX509Cert = Components.interfaces.nsIX509Cert;
     const nsIX509CertDB = Components.interfaces.nsIX509CertDB;
     const nsX509CertDB = "@mozilla.org/security/x509certdb;1";
     const nsISSLStatusProvider = Components.interfaces.nsISSLStatusProvider;
     const nsISSLStatus = Components.interfaces.nsISSLStatus;
 
     // We don't have separate info for a frame, return null until further notice
     // (see bug 138479)
-    if (gWindow != gWindow.top)
+    if (!this.windowInfo.isTopWindow)
       return null;
 
-    var hName = null;
-    try {
-      hName = gWindow.location.host;
-    }
-    catch (exception) { }
+    var hostName = this.windowInfo.hostName;
 
     var ui = security._getSecurityUI();
     if (!ui)
       return null;
 
     var isBroken =
       (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_BROKEN);
     var isMixed =
@@ -51,26 +52,25 @@ var security = {
 
     if (!isInsecure && status) {
       status.QueryInterface(nsISSLStatus);
       var cert = status.serverCert;
       var issuerName =
         this.mapIssuerOrganization(cert.issuerOrganization) || cert.issuerName;
 
       var retval = {
-        hostName : hName,
+        hostName : hostName,
         cAName : issuerName,
         encryptionAlgorithm : undefined,
         encryptionStrength : undefined,
         version: undefined,
         isBroken : isBroken,
         isMixed : isMixed,
         isEV : isEV,
-        cert : cert,
-        fullLocation : gWindow.location
+        cert : cert
       };
 
       var version;
       try {
         retval.encryptionAlgorithm = status.cipherName;
         retval.encryptionStrength = status.secretKeyLength;
         version = status.protocolVersion;
       }
@@ -90,26 +90,26 @@ var security = {
         case nsISSLStatus.TLS_VERSION_1_2:
           retval.version = "TLS 1.2"
           break;
       }
 
       return retval;
     } else {
       return {
-        hostName : hName,
+        hostName : hostName,
         cAName : "",
         encryptionAlgorithm : "",
         encryptionStrength : 0,
         version: "",
         isBroken : isBroken,
         isMixed : isMixed,
         isEV : isEV,
-        cert : null,
-        fullLocation : gWindow.location
+        cert : null
+
       };
     }
   },
 
   // Find the secureBrowserUI object (if present)
   _getSecurityUI : function() {
     if (window.opener.gBrowser)
       return window.opener.gBrowser.securityUI;
@@ -135,23 +135,22 @@ var security = {
   {
     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                        .getService(Components.interfaces.nsIWindowMediator);
     var win = wm.getMostRecentWindow("Browser:Cookies");
     var eTLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"].
                       getService(Components.interfaces.nsIEffectiveTLDService);
 
     var eTLD;
-    var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
     try {
-      eTLD = eTLDService.getBaseDomain(uri);
+      eTLD = eTLDService.getBaseDomain(this.uri);
     }
     catch (e) {
       // getBaseDomain will fail if the host is an IP address or is empty
-      eTLD = uri.asciiHost;
+      eTLD = this.uri.asciiHost;
     }
 
     if (win) {
       win.gCookiesWindow.setFilter(eTLD);
       win.focus();
     }
     else
       window.openDialog("chrome://browser/content/preferences/cookies.xul",
@@ -163,17 +162,19 @@ var security = {
    */
   viewPasswords : function() {
     LoginHelper.openPasswordManager(window, this._getSecurityInfo().hostName);
   },
 
   _cert : null
 };
 
-function securityOnLoad() {
+function securityOnLoad(uri, windowInfo) {
+  security.init(uri, windowInfo);
+
   var info = security._getSecurityInfo();
   if (!info) {
     document.getElementById("securityTab").hidden = true;
     return;
   }
   else {
     document.getElementById("securityTab").hidden = false;
   }
@@ -221,17 +222,16 @@ function securityOnLoad() {
   }
   else
     viewCert.collapsed = true;
 
   /* Set Privacy & History section text */
   var yesStr = pageInfoBundle.getString("yes");
   var noStr = pageInfoBundle.getString("no");
 
-  var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
   setText("security-privacy-cookies-value",
           hostHasCookies(uri) ? yesStr : noStr);
   setText("security-privacy-passwords-value",
           realmHasPasswords(uri) ? yesStr : noStr);
   
   var visitCount = previousVisitCount(info.hostName);
   if(visitCount > 1) {
     setText("security-privacy-history-value",
--- a/browser/base/content/test/general/browser_bug517902.js
+++ b/browser/base/content/test/general/browser_bug517902.js
@@ -9,17 +9,17 @@ function test() {
     gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
 
     var doc = gBrowser.contentDocument;
     var testImg = doc.getElementById("test-image");
     var pageInfo = BrowserPageInfo(doc, "mediaTab", testImg);
 
     pageInfo.addEventListener("load", function () {
       pageInfo.removeEventListener("load", arguments.callee, true);
-      pageInfo.onImagePreviewShown.push(function () {
+      pageInfo.onFinished.push(function () {
         executeSoon(function () {
           var pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
 
           is(pageInfoImg.src, testImg.src, "selected image has the correct source");
           is(pageInfoImg.width, testImg.width, "selected image has the correct width");
           is(pageInfoImg.height, testImg.height, "selected image has the correct height");
 
           pageInfo.close();
--- a/browser/base/content/test/general/browser_bug537013.js
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -10,17 +10,18 @@ let texts = [
   "Klein bottle for sale. Inquire within.",
   "To err is human; to forgive is not company policy."
 ];
 
 let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
 let HasFindClipboard = Clipboard.supportsFindClipboard();
 
 function addTabWithText(aText, aCallback) {
-  let newTab = gBrowser.addTab("data:text/html,<h1 id='h1'>" + aText + "</h1>");
+  let newTab = gBrowser.addTab("data:text/html;charset=utf-8,<h1 id='h1'>" +
+                               aText + "</h1>");
   tabs.push(newTab);
   gBrowser.selectedTab = newTab;
 }
 
 function setFindString(aString) {
   gFindBar.open();
   gFindBar._findField.focus();
   gFindBar._findField.select();
@@ -73,16 +74,22 @@ function continueTests1() {
 
   // While we're here, let's test bug 253793
   gBrowser.reload();
   gBrowser.addEventListener("DOMContentLoaded", continueTests2, true);
 }
 
 function continueTests2() {
   gBrowser.removeEventListener("DOMContentLoaded", continueTests2, true);
+  waitForCondition(() => !gFindBar.getElement("highlight").checked,
+                   continueTests3,
+                   "Highlight never reset!");
+}
+
+function continueTests3() {
   ok(!gFindBar.getElement("highlight").checked, "Highlight button reset!");
   gFindBar.close();
   ok(gFindBar.hidden, "First tab doesn't show find bar!");
   gBrowser.selectedTab = tabs[1];
   ok(!gFindBar.hidden, "Second tab shows find bar!");
   // Test for bug 892384
   is(gFindBar._findField.getAttribute("focused"), "true",
      "Open findbar refocused on tab change!");
--- a/browser/base/content/test/general/browser_bug590206.js
+++ b/browser/base/content/test/general/browser_bug590206.js
@@ -1,138 +1,140 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
+/*
+ * Test the identity mode UI for a variety of page types
  */
 
 const DUMMY = "browser/browser/base/content/test/general/dummy_page.html";
 
-function loadNewTab(aURL, aCallback) {
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(aURL);
-
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    if (gBrowser.selectedBrowser.currentURI.spec != aURL)
-      return;
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-
-    aCallback(gBrowser.selectedTab);
-  }, true);
+function loadNewTab(url) {
+  return BrowserTestUtils.openNewForegroundTab(gBrowser, url);
 }
 
 function getIdentityMode() {
   return document.getElementById("identity-box").className;
 }
 
-var TESTS = [
-function test_webpage() {
+// This test is slow on Linux debug e10s
+requestLongerTimeout(2);
+
+add_task(function* test_webpage() {
+  let oldTab = gBrowser.selectedTab;
+
+  let newTab = yield loadNewTab("http://example.com/" + DUMMY);
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_blank() {
   let oldTab = gBrowser.selectedTab;
 
-  loadNewTab("http://example.com/" + DUMMY, function(aNewTab) {
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  let newTab = yield loadNewTab("about:blank");
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-    gBrowser.selectedTab = oldTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-    gBrowser.selectedTab = aNewTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
-
-    gBrowser.removeTab(aNewTab);
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-    runNextTest();
-  });
-},
+  gBrowser.removeTab(newTab);
+});
 
-function test_blank() {
+add_task(function* test_chrome() {
   let oldTab = gBrowser.selectedTab;
 
-  loadNewTab("about:blank", function(aNewTab) {
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  let newTab = yield loadNewTab("chrome://mozapps/content/extensions/extensions.xul");
+  is(getIdentityMode(), "fileURI", "Identity should be file");
 
-    gBrowser.selectedTab = oldTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-    gBrowser.selectedTab = aNewTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
-
-    gBrowser.removeTab(aNewTab);
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "fileURI", "Identity should be file");
 
-    runNextTest();
-  });
-},
+  gBrowser.removeTab(newTab);
+});
 
-function test_chrome() {
+add_task(function* test_https() {
   let oldTab = gBrowser.selectedTab;
 
-  // Since users aren't likely to type in full chrome URLs, we won't show
-  // the positive security indicator on it, but we will show it on about:addons.
-  loadNewTab("chrome://mozapps/content/extensions/extensions.xul", function(aNewTab) {
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
-
-    gBrowser.selectedTab = oldTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  let newTab = yield loadNewTab("https://example.com/" + DUMMY);
+  is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
 
-    gBrowser.selectedTab = aNewTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
-
-    gBrowser.removeTab(aNewTab);
-
-    runNextTest();
-  });
-},
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-function test_https() {
-  let oldTab = gBrowser.selectedTab;
-
-  loadNewTab("https://example.com/" + DUMMY, function(aNewTab) {
-    is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
-
-    gBrowser.selectedTab = oldTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
 
-    gBrowser.selectedTab = aNewTab;
-    is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
-
-    gBrowser.removeTab(aNewTab);
+  gBrowser.removeTab(newTab);
+});
 
-    runNextTest();
-  });
-},
-
-function test_addons() {
+add_task(function* test_addons() {
   let oldTab = gBrowser.selectedTab;
 
-  loadNewTab("about:addons", function(aNewTab) {
-    is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+  let newTab = yield loadNewTab("about:addons");
+  is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-    gBrowser.selectedTab = oldTab;
-    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "chromeUI", "Identity should be chrome");
 
-    gBrowser.selectedTab = aNewTab;
-    is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+  gBrowser.removeTab(newTab);
+});
 
-    gBrowser.removeTab(aNewTab);
+add_task(function* test_file() {
+  let oldTab = gBrowser.selectedTab;
+  let fileURI = getTestFilePath("");
+
+  let newTab = yield loadNewTab(fileURI);
+  is(getIdentityMode(), "fileURI", "Identity should be file");
 
-    runNextTest();
-  });
-}
-];
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-var gTestStart = null;
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "fileURI", "Identity should be file");
+
+  gBrowser.removeTab(newTab);
+});
 
-function runNextTest() {
-  if (gTestStart)
-    info("Test part took " + (Date.now() - gTestStart) + "ms");
+add_task(function test_resource_uri() {
+  let oldTab = gBrowser.selectedTab;
+  let dataURI = "resource://gre/modules/Services.jsm"
+
+  let newTab = yield loadNewTab(dataURI);
 
-  if (TESTS.length == 0) {
-    finish();
-    return;
-  }
+  is(getIdentityMode(), "fileURI", "Identity should be unknown");
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "fileURI", "Identity should be unknown");
 
-  info("Running " + TESTS[0].name);
-  gTestStart = Date.now();
-  TESTS.shift()();
-};
+  gBrowser.removeTab(newTab);
+});
+
+add_task(function test_data_uri() {
+  let oldTab = gBrowser.selectedTab;
+  let dataURI = "data:text/html,hi"
 
-function test() {
-  waitForExplicitFinish();
+  let newTab = yield loadNewTab(dataURI);
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
-  runNextTest();
-}
+  gBrowser.selectedTab = newTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.removeTab(newTab);
+});
--- a/browser/base/content/test/general/browser_contentSearchUI.js
+++ b/browser/base/content/test/general/browser_contentSearchUI.js
@@ -97,17 +97,17 @@ add_task(function* rightLeftKeys() {
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoo", ["xfoo", "xbar"], 0);
 
   // This should make the xfoo suggestion sticky.  To make sure it sticks,
   // trigger suggestions again and cycle through them by pressing Down until
   // nothing is selected again.
   state = yield msg("key", "VK_RIGHT");
-  checkState(state, "xfoo", [], 0);
+  checkState(state, "xfoo", [], -1);
 
   state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
 
   state = yield msg("key", "VK_DOWN");
@@ -120,30 +120,212 @@ add_task(function* rightLeftKeys() {
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
 
   yield msg("reset");
 });
 
+add_task(function* tabKey() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  let state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+
+  state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+  checkState(state, "x", [], -1);
+
+  yield setUp();
+
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+  for (let i = 0; i < 3; ++i) {
+    state = yield msg("key", "VK_TAB");
+  }
+  checkState(state, "x", [], -1);
+
+  yield setUp();
+
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_UP");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", [], -1);
+
+  yield msg("reset");
+});
+
+add_task(function* cycleSuggestions() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  let cycle = Task.async(function* (aSelectedButtonIndex) {
+    let modifiers = {
+      shiftKey: true,
+      accelKey: true,
+    };
+  
+    let state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+  
+    state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+    checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+  });
+  
+  yield cycle();
+
+  // Repeat with a one-off selected.
+  let state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+  yield cycle(0);
+
+  // Repeat with the settings button selected.
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+  yield cycle(1);
+
+  yield msg("reset");
+});
+
+add_task(function* cycleOneOffs() {
+  yield setUp();
+  yield msg("key", { key: "x", waitForSuggestions: true });
+
+  yield msg("addDuplicateOneOff");
+
+  let state = yield msg("key", "VK_DOWN");
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  let modifiers = {
+    altKey: true,
+  };
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+  // If the settings button is selected, pressing alt+up/down should select the
+  // last/first one-off respectively (and deselect the settings button).
+  yield msg("key", "VK_TAB");
+  yield msg("key", "VK_TAB");
+  state = yield msg("key", "VK_TAB"); // Settings button selected.
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+  state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+  state = yield msg("key", "VK_TAB");
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+  state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+  checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+  yield msg("removeLastOneOff");
+  yield msg("reset");
+});
+
 add_task(function* mouse() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
-  for (let i = 0; i < 4; ++i) {
-    state = yield msg("mousemove", i);
-    checkState(state, "x", ["xfoo", "xbar"], i);
-  }
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  state = yield msg("mousemove", 1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
+  state = yield msg("mousemove", 2);
+  checkState(state, "x", ["xfoo", "xbar"], 1, 0);
+
+  state = yield msg("mousemove", 3);
+  checkState(state, "x", ["xfoo", "xbar"], 1, 1);
 
   state = yield msg("mousemove", -1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
+  yield msg("reset");
+  yield setUp();
+
+  state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  state = yield msg("mousemove", 2);
+  checkState(state, "x", ["xfoo", "xbar"], 0, 0);
+
+  state = yield msg("mousemove", -1);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
   yield msg("reset");
 });
 
 add_task(function* formHistory() {
   yield setUp();
 
   // Type an X and add it to form history.
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
@@ -192,16 +374,43 @@ add_task(function* formHistory() {
 
   // Type an X again.  The form history entry should still be gone.
   state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
+add_task(function* cycleEngines() {
+  yield setUp();
+  yield msg("key", "VK_DOWN");
+
+  function promiseEngineChange(newEngineName) {
+    let deferred = Promise.defer();
+    Services.obs.addObserver(function resolver(subj, topic, data) {
+      if (data != "engine-current") {
+        return;
+      }
+      is(subj.name, newEngineName, "Engine cycled correctly");
+      Services.obs.removeObserver(resolver, "browser-search-engine-modified");
+      deferred.resolve();
+    }, "browser-search-engine-modified", false);
+  }
+
+  let p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME);
+  yield msg("key", { key: "VK_DOWN", modifiers: { accelKey: true }});
+  yield p;
+
+  p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME);
+  yield msg("key", { key: "VK_UP", modifiers: { accelKey: true }});
+  yield p;
+
+  yield msg("reset");
+});
+
 add_task(function* search() {
   yield setUp();
 
   let modifiers = {};
   ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(k => modifiers[k] = true);
 
   // Test typing a query and pressing enter.
   let p = msg("waitForSearch");
@@ -292,29 +501,67 @@ add_task(function* search() {
   eventData.searchString = "x";
   eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
   delete eventData.selection;
   SimpleTest.isDeeply(eventData, mesg, "Search event data");
 
   yield promiseTab();
   yield setUp();
 
+  // Test selecting a suggestion, then clicking a one-off without deselecting the
+  // suggestion.
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("mousemove", 1);
+  yield msg("mousemove", 3);
+  yield msg("click", { eltIdx: 3, modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "xfoo"
+  eventData.selection = {
+    index: 1,
+    kind: "mouse",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Same as above, but with the keyboard.
+  delete modifiers.button;
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_TAB");
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  mesg = yield p;
+  eventData.selection = {
+    index: 1,
+    kind: "key",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
   // Test searching when using IME composition.
   let state = yield msg("startComposition", { data: "" });
   checkState(state, "", [], -1);
   state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
   checkState(state, "x", [{ str: "x", type: "formHistory" },
                           { str: "xfoo", type: "formHistory" }, "xbar"], -1);
   yield msg("commitComposition");
   delete modifiers.button;
   p = msg("waitForSearch");
   yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
   mesg = yield p;
+  eventData.searchString = "x"
   eventData.originalEvent = modifiers;
   eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+  delete eventData.selection;
   SimpleTest.isDeeply(eventData, mesg, "Search event data");
 
   yield promiseTab();
   yield setUp();
 
   state = yield msg("startComposition", { data: "" });
   checkState(state, "", [], -1);
   state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
@@ -423,36 +670,49 @@ function msg(type, data=null) {
     }
     gMsgMan.removeMessageListener(TEST_MSG, onMsg);
     deferred.resolve(msg.data.data);
   });
   return deferred.promise;
 }
 
 function checkState(actualState, expectedInputVal, expectedSuggestions,
-                    expectedSelectedIdx) {
+                    expectedSelectedIdx, expectedSelectedButtonIdx) {
   expectedSuggestions = expectedSuggestions.map(sugg => {
     return typeof(sugg) == "object" ? sugg : {
       str: sugg,
       type: "remote",
     };
   });
 
+  if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
+    expectedSelectedIdx = expectedSuggestions.length + expectedSelectedButtonIdx;
+  }
+  
   let expectedState = {
     selectedIndex: expectedSelectedIdx,
     numSuggestions: expectedSuggestions.length,
     suggestionAtIndex: expectedSuggestions.map(s => s.str),
     isFormHistorySuggestionAtIndex:
       expectedSuggestions.map(s => s.type == "formHistory"),
 
     tableHidden: expectedSuggestions.length == 0,
 
     inputValue: expectedInputVal,
     ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
   };
+  if (expectedSelectedButtonIdx != undefined) {
+    expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
+  }
+  else if (expectedSelectedIdx < expectedSuggestions.length) {
+    expectedState.selectedButtonIndex = -1;
+  }
+  else {
+    expectedState.selectedButtonIndex = expectedSelectedIdx - expectedSuggestions.length;
+  }
 
   SimpleTest.isDeeply(actualState, expectedState, "State");
 }
 
 var gMsgMan;
 
 function promiseTab() {
   let deferred = Promise.defer();
--- a/browser/base/content/test/general/browser_pageInfo.js
+++ b/browser/base/content/test/general/browser_pageInfo.js
@@ -10,17 +10,17 @@ function test() {
     Services.obs.addObserver(observer, "page-info-dialog-loaded", false);
     pageInfo = BrowserPageInfo();
   }, true);
   content.location =
     "https://example.com/browser/browser/base/content/test/general/feed_tab.html";
 
   function observer(win, topic, data) {
     Services.obs.removeObserver(observer, "page-info-dialog-loaded");
-    handlePageInfo();
+    pageInfo.onFinished.push(handlePageInfo);
   }
 
   function handlePageInfo() {
     ok(pageInfo.document.getElementById("feedTab"), "Feed tab");
     let feedListbox = pageInfo.document.getElementById("feedListbox");
     ok(feedListbox, "Feed list");
 
     var feedRowsNum = feedListbox.getRowCount();
--- a/browser/base/content/test/general/browser_trackingUI_5.js
+++ b/browser/base/content/test/general/browser_trackingUI_5.js
@@ -17,16 +17,21 @@ registerCleanupFunction(function() {
 
 function hidden(sel) {
   let win = browser.ownerGlobal;
   let el = win.document.querySelector(sel);
   let display = win.getComputedStyle(el).getPropertyValue("display", null);
   return display === "none";
 }
 
+function identityPopupState() {
+  let win = browser.ownerGlobal;
+  return win.document.getElementById("identity-popup").state;
+}
+
 function clickButton(sel) {
   let win = browser.ownerGlobal;
   let el = win.document.querySelector(sel);
   el.doCommand();
 }
 
 function testTrackingPage(window) {
   info("Tracking content must be blocked");
@@ -80,16 +85,18 @@ add_task(function* testExceptionAddition
   info("Load a test page containing tracking elements");
   yield promiseTabLoadEvent(tab, TRACKING_PAGE);
 
   testTrackingPage(tab.ownerDocument.defaultView);
 
   info("Disable TP for the page (which reloads the page)");
   let tabReloadPromise = promiseTabLoadEvent(tab);
   clickButton("#tracking-action-unblock");
+  is(identityPopupState(), "closed", "foobar");
+
   yield tabReloadPromise;
   testTrackingPageUnblocked();
 
   info("Test that the exception is remembered across tabs in the same private window");
   tab = browser.selectedTab = browser.addTab();
 
   info("Load a test page containing tracking elements");
   yield promiseTabLoadEvent(tab, TRACKING_PAGE);
@@ -110,13 +117,15 @@ add_task(function* testExceptionPersiste
   info("Load a test page containing tracking elements");
   yield promiseTabLoadEvent(tab, TRACKING_PAGE);
 
   testTrackingPage(tab.ownerDocument.defaultView);
 
   info("Disable TP for the page (which reloads the page)");
   let tabReloadPromise = promiseTabLoadEvent(tab);
   clickButton("#tracking-action-unblock");
+  is(identityPopupState(), "closed", "foobar");
+
   yield tabReloadPromise;
   testTrackingPageUnblocked();
 
   privateWin.close();
 });
--- a/browser/base/content/test/general/contentSearchUI.js
+++ b/browser/base/content/test/general/contentSearchUI.js
@@ -119,21 +119,37 @@ let messageHandlers = {
     ack("click");
   },
 
   addInputValueToFormHistory: function () {
     gController.addInputValueToFormHistory();
     ack("addInputValueToFormHistory");
   },
 
+  addDuplicateOneOff: function () {
+    let btn = gController._oneOffButtons[gController._oneOffButtons.length - 1];
+    let newBtn = btn.cloneNode(true);
+    btn.parentNode.appendChild(newBtn);
+    gController._oneOffButtons.push(newBtn);
+    ack("addDuplicateOneOff");
+  },
+
+  removeLastOneOff: function () {
+    gController._oneOffButtons.pop().remove();
+    ack("removeLastOneOff");
+  },
+
   reset: function () {
-    // Reset both the input and suggestions by select all + delete.
+    // Reset both the input and suggestions by select all + delete. If there was
+    // no text entered, this won't have any effect, so also escape to ensure the
+    // suggestions table is closed.
     gController.input.focus();
     content.synthesizeKey("a", { accelKey: true });
     content.synthesizeKey("VK_DELETE", {});
+    content.synthesizeKey("VK_ESCAPE", {});
     ack("reset");
   },
 };
 
 function ack(aType, aData) {
   sendAsyncMessage(TEST_MSG, { type: aType, data: aData || currentState() });
 }
 
@@ -160,16 +176,17 @@ function waitForContentSearchEvent(messa
     mm.removeMessageListener("ContentSearch", listener);
     cb(aMsg.data.data);
   });
 }
 
 function currentState() {
   let state = {
     selectedIndex: gController.selectedIndex,
+    selectedButtonIndex: gController.selectedButtonIndex,
     numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
     suggestionAtIndex: [],
     isFormHistorySuggestionAtIndex: [],
 
     tableHidden: gController._table.hidden,
 
     inputValue: gController.input.value,
     ariaExpanded: gController.input.getAttribute("aria-expanded"),
--- a/browser/base/content/test/plugins/browser_pageInfo_plugins.js
+++ b/browser/base/content/test/plugins/browser_pageInfo_plugins.js
@@ -29,17 +29,17 @@ function doOnOpenPageInfo(continuation) 
   // windows if we don't keep a reference to every window we've opened.
   // So, don't reuse pointers to opened Page Info windows - simply append
   // to this list.
   gPageInfo = BrowserPageInfo(null, "permTab");
 }
 
 function pageInfoObserve(win, topic, data) {
   Services.obs.removeObserver(pageInfoObserve, "page-info-dialog-loaded");
-  executeSoon(gNextTest);
+  gPageInfo.onFinished.push(() => executeSoon(gNextTest));
 }
 
 function finishTest() {
   gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gTestPermissionString);
   gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gSecondTestPermissionString);
   Services.prefs.clearUserPref("plugins.click_to_play");
   gBrowser.removeCurrentTab();
 
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -22,16 +22,18 @@
         <vbox id="identity-popup-security-content" flex="1">
           <label class="identity-popup-headline" crop="end">
             <observes element="identity-popup-content-host" attribute="value"/>
           </label>
           <label class="identity-popup-connection-secure identity-popup-text"
                  value="&identity.connectionSecure;"/>
           <label class="identity-popup-connection-not-secure identity-popup-text"
                  value="&identity.connectionNotSecure;"/>
+          <label class="identity-popup-connection-file-uri identity-popup-text"
+                 value="&identity.connectionFile;"/>
           <label class="identity-popup-connection-internal identity-popup-text"
                  value="&identity.connectionInternal;"/>
         </vbox>
         <button class="identity-popup-expander"
                 oncommand="gIdentityHandler.toggleSubView('security', this)"/>
       </hbox>
 
       <!-- Tracking Protection Section -->
@@ -90,16 +92,18 @@
       <vbox id="identity-popup-securityView-header">
         <label class="identity-popup-headline" crop="end">
           <observes element="identity-popup-content-host" attribute="value"/>
         </label>
         <label class="identity-popup-connection-secure identity-popup-text"
                value="&identity.connectionSecure;"/>
         <label class="identity-popup-connection-not-secure identity-popup-text"
                value="&identity.connectionNotSecure;"/>
+        <label class="identity-popup-connection-file-uri identity-popup-text"
+               value="&identity.connectionFile;"/>
         <label class="identity-popup-connection-internal identity-popup-text"
                value="&identity.connectionInternal;"/>
       </vbox>
 
       <description id="identity-popup-content-verifier"
                    class="identity-popup-text"/>
 
       <description id="identity-popup-securityView-connection"
--- a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
+++ b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
@@ -116,22 +116,18 @@ function configureFxAccountIdentity() {
   let MockInternal = {
     newAccountState(credentials) {
       isnot(credentials, "not expecting credentials");
       let storageManager = new MockFxaStorageManager();
       // and init storage with our user.
       storageManager.initialize(user);
       return new AccountState(storageManager);
     },
-    getCertificate(data, keyPair, mustBeValidUntil) {
-      this.cert = {
-        validUntil: this.now() + 10000,
-        cert: "certificate",
-      };
-      return Promise.resolve(this.cert.cert);
+    _getAssertion(audience) {
+      return Promise.resolve("assertion");
     },
     getCertificateSigned() {
       return Promise.resolve();
     },
   };
   let mockTSC = { // TokenServerClient
     getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
       token.uid = "username";
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -177,16 +177,20 @@ this.SessionStore = {
   get promiseInitialized() {
     return SessionStoreInternal.promiseInitialized;
   },
 
   get canRestoreLastSession() {
     return SessionStoreInternal.canRestoreLastSession;
   },
 
+  get crashedTabCount() {
+    return SessionStoreInternal._crashedBrowsersCount;
+  },
+
   set canRestoreLastSession(val) {
     SessionStoreInternal.canRestoreLastSession = val;
   },
 
   init: function ss_init() {
     SessionStoreInternal.init();
   },
 
@@ -297,16 +301,20 @@ this.SessionStore = {
   getCurrentState: function (aUpdateAll) {
     return SessionStoreInternal.getCurrentState(aUpdateAll);
   },
 
   reviveCrashedTab(aTab) {
     return SessionStoreInternal.reviveCrashedTab(aTab);
   },
 
+  reviveAllCrashedTabs() {
+    return SessionStoreInternal.reviveAllCrashedTabs();
+  },
+
   navigateAndRestore(tab, loadArguments, historyIndex) {
     return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex);
   }
 };
 
 // Freeze the SessionStore object. We don't want anyone to modify it.
 Object.freeze(SessionStore);
 
@@ -326,16 +334,19 @@ let SessionStoreInternal = {
   // For each <browser> element, records the current epoch.
   _browserEpochs: new WeakMap(),
 
   // Any browsers that fires the oop-browser-crashed event gets stored in
   // here - that way we know which browsers to ignore messages from (until
   // they get restored).
   _crashedBrowsers: new WeakSet(),
 
+  // The number of crashed browsers.
+  _crashedBrowsersCount: 0,
+
   // A map (xul:browser -> nsIFrameLoader) that maps a browser to the last
   // associated frameLoader we heard about.
   _lastKnownFrameLoader: new WeakMap(),
 
   // A map (xul:browser -> object) that maps a browser associated with a
   // recently closed tab to all its necessary state information we need to
   // properly handle final update message.
   _closedTabs: new WeakMap(),
@@ -1403,16 +1414,20 @@ let SessionStoreInternal = {
 
     if (browser.frameLoader) {
       this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
     }
 
     if (!aNoNotification) {
       this.saveStateDelayed(aWindow);
     }
+
+    if (this._crashedBrowsers.has(browser.permanentKey)) {
+      this._crashedBrowsersCount++;
+    }
   },
 
   /**
    * remove listeners for a tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
@@ -1432,16 +1447,20 @@ let SessionStoreInternal = {
       this._resetTabRestoringState(aTab);
       if (previousState == TAB_STATE_RESTORING)
         this.restoreNextTab();
     }
 
     if (!aNoNotification) {
       this.saveStateDelayed(aWindow);
     }
+
+    if (this._crashedBrowsers.has(browser.permanentKey)) {
+      this._crashedBrowsersCount--;
+    }
   },
 
   /**
    * When a tab closes, collect its properties
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
@@ -1611,16 +1630,17 @@ let SessionStoreInternal = {
    *
    * @param aWindow
    *        The window that the crashed browser belongs to.
    * @param aBrowser
    *        The <xul:browser> that is now in the crashed state.
    */
   onBrowserCrashed: function(aWindow, aBrowser) {
     this._crashedBrowsers.add(aBrowser.permanentKey);
+    this._crashedBrowsersCount++;
     // If we never got around to restoring this tab, clear its state so
     // that we don't try restoring if the user switches to it before
     // reviving the crashed browser. This is throwing away the information
     // that the tab was in a pending state when the browser crashed, which
     // is an explicit choice. For now, when restoring all crashed tabs, based
     // on a user preference we'll either restore all of them at once, or only
     // restore the selected tab and lazily restore the rest. We'll make no
     // efforts at this time to be smart and restore all of the tabs that had
@@ -2166,16 +2186,33 @@ let SessionStoreInternal = {
     // at this point.
     if (browser.isRemoteBrowser) {
       throw new Error("SessionStore.reviveCrashedTab: " +
                       "Somehow a crashed browser is still remote.")
     }
 
     let data = TabState.collect(aTab);
     this.restoreTab(aTab, data);
+
+    this._crashedBrowsersCount--;
+  },
+
+  /**
+   * Revive all crashed tabs and reset the crashed tabs count to 0.
+   */
+  reviveAllCrashedTabs() {
+    let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+    while (windowsEnum.hasMoreElements()) {
+      let window = windowsEnum.getNext();
+      for (let tab of window.gBrowser.tabs) {
+        this.reviveCrashedTab(tab);
+      }
+    }
+
+    this._crashedBrowsersCount = 0;
   },
 
   /**
    * Navigate the given |tab| by first collecting its current state and then
    * either changing only the index of the currently shown shistory entry,
    * or restoring the exact same state again and passing the new URL to load
    * in |loadArguments|. Use this method to seamlessly switch between pages
    * loaded in the parent and pages loaded in the child process.
--- a/browser/components/sessionstore/test/browser_crashedTabs.js
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -336,8 +336,54 @@ add_task(function test_close_tab_after_c
   let promise = promiseEvent(gBrowser.tabContainer, "TabClose");
 
   // Click the close tab button
   clickButton(browser, "closeTab");
   yield promise;
 
   is(gBrowser.tabs.length, 1, "Should have closed the tab");
 });
+
+/**
+ * Checks that "restore all" button is only shown if more than one tab
+ * has crashed.
+ */
+add_task(function* test_hide_restore_all_button() {
+  let newTab = gBrowser.addTab();
+  gBrowser.selectedTab = newTab;
+  let browser = newTab.linkedBrowser;
+  ok(browser.isRemoteBrowser, "Should be a remote browser");
+  yield promiseBrowserLoaded(browser);
+
+  browser.loadURI(PAGE_1);
+  yield promiseBrowserLoaded(browser);
+
+  yield TabStateFlusher.flush(browser);
+
+  // Crash the tab
+  yield crashBrowser(browser);
+
+  let doc = browser.contentDocument;
+  let restoreAllButton = doc.getElementById("restoreAll");
+  let restoreOneButton = doc.getElementById("restoreTab");
+
+  is(restoreAllButton.getAttribute("hidden"), "true", "Restore All button should be hidden");
+  ok(restoreOneButton.classList.contains("primary"), "Restore Tab button should have the primary class");
+
+  let newTab2 = gBrowser.addTab();
+  gBrowser.selectedTab = newTab;
+
+  browser.loadURI(PAGE_2);
+  yield promiseBrowserLoaded(browser);
+
+  // Crash the tab
+  yield crashBrowser(browser);
+
+  doc = browser.contentDocument;
+  restoreAllButton = doc.getElementById("restoreAll");
+  restoreOneButton = doc.getElementById("restoreTab");
+
+  ok(!restoreAllButton.hasAttribute("hidden"), "Restore All button should not be hidden");
+  ok(!(restoreOneButton.classList.contains("primary")), "Restore Tab button should not have the primary class");
+
+  gBrowser.removeTab(newTab);
+  gBrowser.removeTab(newTab2);
+});
--- a/browser/devtools/shared/test/browser_outputparser.js
+++ b/browser/devtools/shared/test/browser_outputparser.js
@@ -13,17 +13,17 @@ add_task(function*() {
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
   let [host, , doc] = yield createHost("bottom", "data:text/html," +
     "<h1>browser_outputParser.js</h1><div></div>");
 
-  let parser = new OutputParser();
+  let parser = new OutputParser(doc);
   testParseCssProperty(doc, parser);
   testParseCssVar(doc, parser);
 
   host.destroy();
 }
 
 // Class name used in color swatch.
 let COLOR_TEST_CLASS = "test-class";
--- a/browser/devtools/styleeditor/styleeditor.xul
+++ b/browser/devtools/styleeditor/styleeditor.xul
@@ -129,17 +129,17 @@
       <xul:box class="splitview-side-details devtools-main-content"/>
 
       <div id="splitview-templates" hidden="true">
         <li id="splitview-tpl-summary-stylesheet" tabindex="0">
           <xul:label class="stylesheet-enabled" tabindex="0"
             tooltiptext="&visibilityToggle.tooltip;"
             accesskey="&saveButton.accesskey;"></xul:label>
           <hgroup class="stylesheet-info">
-            <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1>
+            <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="center"/></a></h1>
             <div class="stylesheet-more">
               <h3 class="stylesheet-title"></h3>
               <h3 class="stylesheet-linked-file"></h3>
               <h3 class="stylesheet-rule-count"></h3>
               <xul:spacer/>
               <h3><xul:label class="stylesheet-saveButton"
                     tooltiptext="&saveButton.tooltip;"
                     accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3>
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -132,17 +132,17 @@ UpdateProcess.prototype = {
 function CssComputedView(inspector, document, pageStyle) {
   this.inspector = inspector;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.pageStyle = pageStyle;
 
   this.propertyViews = [];
 
-  this._outputParser = new OutputParser();
+  this._outputParser = new OutputParser(document);
 
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIXULChromeRegistry);
   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
 
   // Create bound methods.
   this.focusWindow = this.focusWindow.bind(this);
   this._onKeypress = this._onKeypress.bind(this);
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -329,17 +329,21 @@ ElementStyle.prototype = {
     // Gather all the text properties applied by these rules, ordered
     // from more- to less-specific. Text properties from keyframes rule are
     // excluded from being marked as overridden since a number of criteria such
     // as time, and animation overlay are required to be check in order to
     // determine if the property is overridden.
     let textProps = [];
     for (let rule of this.rules) {
       if (rule.pseudoElement == pseudo && !rule.keyframes) {
-        textProps = textProps.concat(rule.textProps.slice(0).reverse());
+        for (let textProp of rule.textProps.slice(0).reverse()) {
+          if (textProp.enabled) {
+            textProps.push(textProp);
+          }
+        }
       }
     }
 
     // Gather all the computed properties applied by those text
     // properties.
     let computedProps = [];
     for (let textProp of textProps) {
       computedProps = computedProps.concat(textProp.computed);
@@ -1156,17 +1160,17 @@ TextProperty.prototype = {
  */
 function CssRuleView(inspector, document, aStore, aPageStyle) {
   this.inspector = inspector;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.store = aStore || {};
   this.pageStyle = aPageStyle;
 
-  this._outputParser = new OutputParser();
+  this._outputParser = new OutputParser(document);
 
   this._onKeypress = this._onKeypress.bind(this);
   this._onAddRule = this._onAddRule.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
   this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
@@ -3158,17 +3162,18 @@ TextPropertyEditor.prototype = {
     // Populate the computed styles.
     this._updateComputed();
 
     // Update the rule property highlight.
     this.ruleView._updatePropertyHighlight(this);
   },
 
   _onStartEditing: function() {
-    this._previewValue(this.prop.value);
+    this.element.classList.remove("ruleview-overridden");
+    this.enable.style.visibility = "hidden";
   },
 
   /**
    * Populate the list of computed styles.
    */
   _updateComputed: function() {
     // Clear out existing viewers.
     while (this.computed.hasChildNodes()) {
@@ -3463,19 +3468,16 @@ TextPropertyEditor.prototype = {
    */
   _previewValue: function(aValue) {
     // Since function call is throttled, we need to make sure we are still
     // editing, and any selector modifications have been completed
     if (!this.editing || this.ruleEditor.isEditing) {
       return;
     }
 
-    this.element.classList.remove("ruleview-overridden");
-    this.enable.style.visibility = "hidden";
-
     let val = parseSingleValue(aValue);
     this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
                                               val.priority);
   },
 
   /**
    * Validate this property. Does it make sense for this value to be assigned
    * to this property name? This does not apply the property value
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -107,27 +107,31 @@ skip-if = e10s # Bug 1039528: "inspect e
 [browser_ruleview_filtereditor-commit-on-ENTER.js]
 [browser_ruleview_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
 [browser_ruleview_inherit.js]
 [browser_ruleview_keybindings.js]
 [browser_ruleview_keyframes-rule_01.js]
 [browser_ruleview_keyframes-rule_02.js]
 [browser_ruleview_livepreview.js]
+[browser_ruleview_mark_overridden_01.js]
+[browser_ruleview_mark_overridden_02.js]
+[browser_ruleview_mark_overridden_03.js]
+[browser_ruleview_mark_overridden_04.js]
+[browser_ruleview_mark_overridden_05.js]
 [browser_ruleview_mathml-element.js]
 [browser_ruleview_media-queries.js]
 [browser_ruleview_multiple-properties-duplicates.js]
 [browser_ruleview_multiple-properties-priority.js]
 [browser_ruleview_multiple-properties-unfinished_01.js]
 [browser_ruleview_multiple-properties-unfinished_02.js]
 [browser_ruleview_multiple_properties_01.js]
 [browser_ruleview_multiple_properties_02.js]
 [browser_ruleview_original-source-link.js]
 [browser_ruleview_cycle-color.js]
-[browser_ruleview_override.js]
 [browser_ruleview_pseudo-element_01.js]
 [browser_ruleview_pseudo-element_02.js]
 skip-if = e10s # Bug 1090340
 [browser_ruleview_pseudo_lock_options.js]
 [browser_ruleview_refresh-on-attribute-change_01.js]
 [browser_ruleview_refresh-on-attribute-change_02.js]
 [browser_ruleview_refresh-on-style-change.js]
 [browser_ruleview_search-filter-computed-list_01.js]
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-property_04.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-property_04.js
@@ -1,16 +1,15 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Tests that a disabled property is previewed when the property name or value
-// editor is focused and the property remains disabled when the escaping out of
+// Tests that a disabled property remains disabled when the escaping out of
 // the property editor.
 
 let TEST_URI = [
   "<style type='text/css'>",
   "#testid {",
   "  background-color: blue;",
   "}",
   "</style>",
@@ -34,42 +33,41 @@ function* testDisableProperty(inspector,
 
   let newValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
     ruleIndex: 0,
     name: "background-color"
   });
   is(newValue, "", "background-color should have been unset.");
 
-  yield testPreviewDisableProperty(view, ruleEditor, propEditor,
+  yield testEditDisableProperty(view, ruleEditor, propEditor,
     propEditor.nameSpan, "VK_ESCAPE");
-  yield testPreviewDisableProperty(view, ruleEditor, propEditor,
+  yield testEditDisableProperty(view, ruleEditor, propEditor,
     propEditor.valueSpan, "VK_ESCAPE");
-  yield testPreviewDisableProperty(view, ruleEditor, propEditor,
+  yield testEditDisableProperty(view, ruleEditor, propEditor,
     propEditor.valueSpan, "VK_TAB");
-  yield testPreviewDisableProperty(view, ruleEditor, propEditor,
+  yield testEditDisableProperty(view, ruleEditor, propEditor,
     propEditor.valueSpan, "VK_RETURN");
 }
 
-function* testPreviewDisableProperty(view, ruleEditor, propEditor,
+function* testEditDisableProperty(view, ruleEditor, propEditor,
     editableField, commitKey) {
   let editor = yield focusEditableField(view, editableField);
-  yield ruleEditor.rule._applyingModifications;
 
   ok(!propEditor.element.classList.contains("ruleview-overridden"),
     "property is not overridden.");
   is(propEditor.enable.style.visibility, "hidden",
     "property enable checkbox is hidden.");
 
   let newValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
     ruleIndex: 0,
     name: "background-color"
   });
-  is(newValue, "blue", "background-color should have been previewed.");
+  is(newValue, "", "background-color should remain unset.");
 
   let onBlur = once(editor.input, "blur");
   EventUtils.synthesizeKey(commitKey, {}, view.styleWindow);
   yield onBlur;
   yield ruleEditor.rule._applyingModifications;
 
   ok(!propEditor.prop.enabled, "property is disabled.");
   ok(propEditor.element.classList.contains("ruleview-overridden"),
@@ -79,10 +77,10 @@ function* testPreviewDisableProperty(vie
   ok(!propEditor.enable.getAttribute("checked"),
     "property enable checkbox is not checked.");
 
   newValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
     ruleIndex: 0,
     name: "background-color"
   });
-  is(newValue, "", "background-color should have been unset.");
+  is(newValue, "", "background-color should remain unset.");
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_mark_overridden_01.js
@@ -0,0 +1,64 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// specificity of the rule
+
+let TEST_URI = [
+  "<style type='text/css'>",
+  "#testid {",
+  "  background-color: blue;",
+  "}",
+  ".testclass {",
+  "  background-color: green;",
+  "}",
+  "</style>",
+  "<div id='testid' class='testclass'>Styled Node</div>"
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  is(idProp.name, "background-color",
+    "First ID property should be background-color");
+  is(idProp.value, "blue", "First ID property value should be blue");
+  ok(!idProp.overridden, "ID prop should not be overridden.");
+  ok(!idProp.editor.element.classList.contains("ruleview-overridden"),
+    "ID property editor should not have ruleview-overridden class");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  is(classProp.name, "background-color",
+    "First class prop should be background-color");
+  is(classProp.value, "green", "First class property value should be green");
+  ok(classProp.overridden, "Class property should be overridden.");
+  ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+    "Class property editor should have ruleview-overridden class");
+
+  // Override background-color by changing the element style.
+  let elementRule = elementStyle.rules[0];
+  elementRule.createProperty("background-color", "purple", "");
+  yield elementRule._applyingModifications;
+
+  let elementProp = elementRule.textProps[0];
+  ok(!elementProp.overridden,
+    "Element style property should not be overridden");
+  ok(idProp.overridden, "ID property should be overridden");
+  ok(idProp.editor.element.classList.contains("ruleview-overridden"),
+    "ID property editor should have ruleview-overridden class");
+  ok(classProp.overridden, "Class property should be overridden");
+  ok(classProp.editor.element.classList.contains("ruleview-overridden"),
+    "Class property editor should have ruleview-overridden class");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_mark_overridden_02.js
@@ -0,0 +1,45 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly for short hand
+// properties and the computed list properties
+
+let TEST_URI = [
+  "<style type='text/css'>",
+  "#testid {",
+  "  margin-left: 1px;",
+  "}",
+  ".testclass {",
+  "  margin: 2px;",
+  "}",
+  "</style>",
+  "<div id='testid' class='testclass'>Styled Node</div>"
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let elementStyle = view._elementStyle;
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden,
+    "Class prop shouldn't be overridden, some props are still being used.");
+
+  for (let computed of classProp.computed) {
+    if (computed.name.indexOf("margin-left") == 0) {
+      ok(computed.overridden, "margin-left props should be overridden.");
+    } else {
+      ok(!computed.overridden,
+        "Non-margin-left props should not be overridden.");
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_mark_overridden_03.js
@@ -0,0 +1,48 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// priority for the rule
+
+let TEST_URI = [
+  "<style type='text/css'>",
+  "#testid {",
+  "  background-color: blue;",
+  "}",
+  ".testclass {",
+  "  background-color: green !important;",
+  "}",
+  "</style>",
+  "<div id='testid' class='testclass'>Styled Node</div>"
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  ok(idProp.overridden, "Not-important rule should be overridden.");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden, "Important rule should not be overridden.");
+
+  let elementRule = elementStyle.rules[0];
+  let elementProp = elementRule.createProperty("background-color", "purple",
+    "important");
+  yield elementRule._applyingModifications;
+
+  ok(!elementProp.overridden, "New important prop should not be overriden.");
+  ok(idProp.overridden, "ID property should be overridden.");
+  ok(classProp.overridden, "Class property should be overridden.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_mark_overridden_04.js
@@ -0,0 +1,42 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly if a property gets
+// disabled
+
+let TEST_URI = [
+  "<style type='text/css'>",
+  "#testid {",
+  "  background-color: blue;",
+  "}",
+  ".testclass {",
+  "  background-color: green;",
+  "}",
+  "</style>",
+  "<div id='testid' class='testclass'>Styled Node</div>"
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+
+  idProp.setEnabled(false);
+  yield idRule._applyingModifications;
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden,
+    "Class prop should not be overridden after id prop was disabled.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_mark_overridden_05.js
@@ -0,0 +1,36 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view marks overridden rules correctly based on the
+// order of the property
+
+let TEST_URI = [
+  "<style type='text/css'>",
+  "#testid {",
+  "  background-color: green;",
+  "}",
+  "</style>",
+  "<div id='testid' class='testclass'>Styled Node</div>"
+].join("\n");
+
+add_task(function*() {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("#testid", inspector);
+  yield testMarkOverridden(inspector, view);
+});
+
+function* testMarkOverridden(inspector, view) {
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+  yield createNewRuleViewProperty(ruleEditor, "background-color: red;");
+
+  let firstProp = ruleEditor.rule.textProps[0];
+  let secondProp = ruleEditor.rule.textProps[1];
+
+  ok(firstProp.overridden, "First property should be overridden.");
+  ok(!secondProp.overridden, "Second property should not be overridden.");
+}
--- a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js
@@ -31,19 +31,19 @@ function waitRuleViewChanged(view, n) {
       deferred.resolve();
     }
   }
   view.on("ruleview-changed", listener);
   return deferred.promise;
 }
 function* testCreateNewMultiUnfinished(inspector, ruleEditor, view) {
   let onMutation = inspector.once("markupmutation");
-  // There is 6 rule-view updates, one for the rule view creation,
-  // one for each new property and one last for throttle update.
-  let onRuleViewChanged = waitRuleViewChanged(view, 6);
+  // There is 5 rule-view updates, one for the rule view creation,
+  // one for each new property
+  let onRuleViewChanged = waitRuleViewChanged(view, 5);
   yield createNewRuleViewProperty(ruleEditor,
     "color:blue;background : orange   ; text-align:center; border-color: ");
   yield onMutation;
   yield onRuleViewChanged;
 
   is(ruleEditor.rule.textProps.length, 4, "Should have created new text properties.");
   is(ruleEditor.propertyList.children.length, 4, "Should have created property editors.");
 
deleted file mode 100644
--- a/browser/devtools/styleinspector/test/browser_ruleview_override.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-// Test the display of overridden declarations in the rule-view
-
-add_task(function*() {
-  yield addTab("data:text/html;charset=utf-8,browser_ruleview_override.js");
-  let {toolbox, inspector, view} = yield openRuleView();
-
-  yield simpleOverride(inspector, view);
-  yield partialOverride(inspector, view);
-  yield importantOverride(inspector, view);
-  yield disableOverride(inspector, view);
-});
-
-function* createTestContent(inspector, style) {
-  let onMutated = inspector.once("markupmutation");
-  let styleNode = addStyle(content.document, style);
-  content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
-  yield onMutated;
-  yield selectNode("#testid", inspector);
-  return styleNode;
-}
-
-function* removeTestContent(inspector, node) {
-  let onMutated = inspector.once("markupmutation");
-  node.remove();
-  yield onMutated;
-}
-
-function* simpleOverride(inspector, view) {
-  let styleNode = yield createTestContent(inspector, '' +
-    '#testid {' +
-    '  background-color: blue;' +
-    '} ' +
-    '.testclass {' +
-    '  background-color: green;' +
-    '}');
-
-  let elementStyle = view._elementStyle;
-
-  let idRule = elementStyle.rules[1];
-  let idProp = idRule.textProps[0];
-  is(idProp.name, "background-color", "First ID prop should be background-color");
-  ok(!idProp.overridden, "ID prop should not be overridden.");
-
-  let classRule = elementStyle.rules[2];
-  let classProp = classRule.textProps[0];
-  is(classProp.name, "background-color", "First class prop should be background-color");
-  ok(classProp.overridden, "Class property should be overridden.");
-
-  // Override background-color by changing the element style.
-  let elementRule = elementStyle.rules[0];
-  elementRule.createProperty("background-color", "purple", "");
-  yield elementRule._applyingModifications;
-
-  let elementProp = elementRule.textProps[0];
-  is(classProp.name, "background-color", "First element prop should now be background-color");
-  ok(!elementProp.overridden, "Element style property should not be overridden");
-  ok(idProp.overridden, "ID property should be overridden");
-  ok(classProp.overridden, "Class property should be overridden");
-
-  yield removeTestContent(inspector, styleNode);
-}
-
-function* partialOverride(inspector, view) {
-  let styleNode = yield createTestContent(inspector, '' +
-    // Margin shorthand property...
-    '.testclass {' +
-    '  margin: 2px;' +
-    '}' +
-    // ... will be partially overridden.
-    '#testid {' +
-    '  margin-left: 1px;' +
-    '}');
-
-  let elementStyle = view._elementStyle;
-
-  let classRule = elementStyle.rules[2];
-  let classProp = classRule.textProps[0];
-  ok(!classProp.overridden,
-    "Class prop shouldn't be overridden, some props are still being used.");
-
-  for (let computed of classProp.computed) {
-    if (computed.name.indexOf("margin-left") == 0) {
-      ok(computed.overridden, "margin-left props should be overridden.");
-    } else {
-      ok(!computed.overridden, "Non-margin-left props should not be overridden.");
-    }
-  }
-
-  yield removeTestContent(inspector, styleNode);
-}
-
-function* importantOverride(inspector, view) {
-  let styleNode = yield createTestContent(inspector, '' +
-    // Margin shorthand property...
-    '.testclass {' +
-    '  background-color: green !important;' +
-    '}' +
-    // ... will be partially overridden.
-    '#testid {' +
-    '  background-color: blue;' +
-    '}');
-
-  let elementStyle = view._elementStyle;
-
-  let idRule = elementStyle.rules[1];
-  let idProp = idRule.textProps[0];
-  ok(idProp.overridden, "Not-important rule should be overridden.");
-
-  let classRule = elementStyle.rules[2];
-  let classProp = classRule.textProps[0];
-  ok(!classProp.overridden, "Important rule should not be overridden.");
-
-  yield removeTestContent(inspector, styleNode);
-
-  let elementRule = elementStyle.rules[0];
-  let elementProp = elementRule.createProperty("background-color", "purple", "important");
-  yield elementRule._applyingModifications;
-
-  ok(classProp.overridden, "New important prop should override class property.");
-  ok(!elementProp.overridden, "New important prop should not be overriden.");
-}
-
-function* disableOverride(inspector, view) {
-  let styleNode = yield createTestContent(inspector, '' +
-    '#testid {' +
-    '  background-color: blue;' +
-    '}' +
-    '.testclass {' +
-    '  background-color: green;' +
-    '}');
-
-  let elementStyle = view._elementStyle;
-
-  let idRule = elementStyle.rules[1];
-  let idProp = idRule.textProps[0];
-
-  idProp.setEnabled(false);
-  yield idRule._applyingModifications;
-
-  let classRule = elementStyle.rules[2];
-  let classProp = classRule.textProps[0];
-  ok(!classProp.overridden, "Class prop should not be overridden after id prop was disabled.");
-
-  yield removeTestContent(inspector, styleNode);
-}
--- a/browser/devtools/styleinspector/test/browser_styleinspector_output-parser.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_output-parser.js
@@ -305,17 +305,17 @@ function test() {
       name: "background",
       value: "rgb(255, var(--g-value, 0), 192)",
       test: fragment => {
 	is(fragment.textContent, "rgb(255, var(--g-value, 0), 192)");
       }
     }
   ];
 
-  let parser = new OutputParser();
+  let parser = new OutputParser(document);
   for (let i = 0; i < testData.length; i ++) {
     let data = testData[i];
     info("Output-parser test data " + i + ". {" + data.name + " : " + data.value + ";}");
     data.test(parser.parseCssProperty(data.name, data.value, {
       colorClass: COLOR_CLASS,
       urlClass: URL_CLASS,
       bezierClass: CUBIC_BEZIER_CLASS,
       defaultColorType: false
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -409,39 +409,25 @@ function* waitForComputedStyleProperty(s
 }
 
 /**
  * Given an inplace editable element, click to switch it to edit mode, wait for
  * focus
  * @return a promise that resolves to the inplace-editor element when ready
  */
 let focusEditableField = Task.async(function*(ruleView, editable, xOffset=1, yOffset=1, options={}) {
-  // Focusing the name or value input is going to fire a preview and update the rule view
-  let expectRuleViewUpdate =
-    editable.classList.contains("ruleview-propertyname") ||
-    editable.classList.contains("ruleview-propertyvalue");
-  let onRuleViewChanged;
-  if (expectRuleViewUpdate) {
-    onRuleViewChanged = ruleView.once("ruleview-changed");
-  }
-
   let onFocus = once(editable.parentNode, "focus", true);
   info("Clicking on editable field to turn to edit mode");
   EventUtils.synthesizeMouse(editable, xOffset, yOffset, options,
     editable.ownerDocument.defaultView);
-  let event = yield onFocus;
+  yield onFocus;
 
   info("Editable field gained focus, returning the input field now");
   let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
 
-  if (expectRuleViewUpdate) {
-    info("Waiting for rule view update");
-    yield onRuleViewChanged;
-  }
-
   return onEdit;
 });
 
 /**
  * Given a tooltip object instance (see Tooltip.js), checks if it is set to
  * toggle and hover and if so, checks if the given target is a valid hover target.
  * This won't actually show the tooltip (the less we interact with XUL panels
  * during test runs, the better).
@@ -796,21 +782,21 @@ function getRuleViewRuleEditor(view, chi
  * @param {RuleEditor} ruleEditor An instance of RuleEditor that will receive
  * the new property
  * @return a promise that resolves to the newly created editor when ready and
  * focused
  */
 let focusNewRuleViewProperty = Task.async(function*(ruleEditor) {
   info("Clicking on a close ruleEditor brace to start editing a new property");
   ruleEditor.closeBrace.scrollIntoView();
-  let editor = yield focusEditableField(ruleEditor.ruleView, ruleEditor.closeBrace);
+  let editor = yield focusEditableField(ruleEditor.ruleView,
+    ruleEditor.closeBrace);
 
-  is(inplaceEditor(ruleEditor.newPropSpan), editor, "Focused editor is the new property editor.");
-  is(ruleEditor.rule.textProps.length,  0, "Starting with one new text property.");
-  is(ruleEditor.propertyList.children.length, 1, "Starting with two property editors.");
+  is(inplaceEditor(ruleEditor.newPropSpan), editor,
+    "Focused editor is the new property editor.");
 
   return editor;
 });
 
 /**
  * Create a new property name in the rule-view, focusing a new property editor
  * by clicking on the close brace, and then entering the given text.
  * Keep in mind that the rule-view knows how to handle strings with multiple
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -675,16 +675,17 @@ you can use these alternative items. Oth
 <!ENTITY spellAddDictionaries.accesskey "A">
 
 <!ENTITY editBookmark.done.label                     "Done">
 <!ENTITY editBookmark.cancel.label                   "Cancel">
 <!ENTITY editBookmark.removeBookmark.accessKey       "R">
 
 <!ENTITY identity.connectionSecure "Secure Connection">
 <!ENTITY identity.connectionNotSecure "Connection is Not Secure">
+<!ENTITY identity.connectionFile "This page is stored on your computer.">
 <!ENTITY identity.connectionVerified "&brandShortName; verified that you are securely connected to this site, run by:">
 <!ENTITY identity.connectionInternal "This is a secure &brandShortName; page.">
 
 <!ENTITY identity.moreInfoLinkText2 "More Information">
 
 <!ENTITY identity.permissions "Permissions">
 
 <!-- Name for the tabs toolbar as spoken by screen readers.
--- a/browser/modules/ContentCrashReporters.jsm
+++ b/browser/modules/ContentCrashReporters.jsm
@@ -83,25 +83,35 @@ this.TabCrashReporter = {
           this.browserMap.delete(browser);
           browser.contentDocument.documentElement.classList.remove("crashDumpAvailable");
           browser.contentDocument.documentElement.classList.add("crashDumpSubmitted");
         }
       }
     }
   },
 
-  onAboutTabCrashedLoad: function (aBrowser) {
+  onAboutTabCrashedLoad: function (aBrowser, aParams) {
+    // If there was only one tab open that crashed, do not show the "restore all tabs" button
+    if (aParams.crashedTabCount == 1) {
+      this.hideRestoreAllButton(aBrowser);
+    }
+
     if (!this.childMap)
       return;
 
     let dumpID = this.childMap.get(this.browserMap.get(aBrowser));
     if (!dumpID)
       return;
 
     aBrowser.contentDocument.documentElement.classList.add("crashDumpAvailable");
+  },
+
+  hideRestoreAllButton: function (aBrowser) {
+    aBrowser.contentDocument.getElementById("restoreAll").setAttribute("hidden", true);
+    aBrowser.contentDocument.getElementById("restoreTab").setAttribute("class", "primary");
   }
 }
 
 this.PluginCrashReporter = {
   /**
    * Makes the PluginCrashReporter ready to hear about and
    * submit crash reports.
    */
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -141,16 +141,17 @@ browser.jar:
   skin/classic/browser/loop/menuPanel.png             (loop/menuPanel.png)
   skin/classic/browser/loop/menuPanel@2x.png          (loop/menuPanel@2x.png)
   skin/classic/browser/loop/toolbar.png               (loop/toolbar.png)
   skin/classic/browser/loop/toolbar@2x.png            (loop/toolbar@2x.png)
   skin/classic/browser/loop/toolbar-inverted.png      (loop/toolbar-inverted.png)
   skin/classic/browser/loop/toolbar-inverted@2x.png   (loop/toolbar-inverted@2x.png)
 * skin/classic/browser/controlcenter/panel.css        (controlcenter/panel.css)
   skin/classic/browser/controlcenter/arrow-subview.svg  (../shared/controlcenter/arrow-subview.svg)
+  skin/classic/browser/controlcenter/arrow-subview-back.svg  (../shared/controlcenter/arrow-subview-back.svg)
   skin/classic/browser/controlcenter/conn-not-secure.svg  (../shared/controlcenter/conn-not-secure.svg)
   skin/classic/browser/controlcenter/conn-secure.svg  (../shared/controlcenter/conn-secure.svg)
   skin/classic/browser/controlcenter/conn-degraded.svg  (../shared/controlcenter/conn-degraded.svg)
   skin/classic/browser/controlcenter/mcb-disabled.svg  (../shared/controlcenter/mcb-disabled.svg)
   skin/classic/browser/controlcenter/permissions.svg  (../shared/controlcenter/permissions.svg)
   skin/classic/browser/controlcenter/tracking-protection.svg                 (../shared/controlcenter/tracking-protection.svg)
   skin/classic/browser/controlcenter/tracking-protection-disabled.svg        (../shared/controlcenter/tracking-protection-disabled.svg)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -185,16 +185,17 @@ browser.jar:
   skin/classic/browser/loop/toolbar-inverted.png      (loop/toolbar-inverted.png)
   skin/classic/browser/loop/toolbar-inverted@2x.png   (loop/toolbar-inverted@2x.png)
   skin/classic/browser/yosemite/loop/menuPanel.png          (loop/menuPanel-yosemite.png)
   skin/classic/browser/yosemite/loop/menuPanel@2x.png       (loop/menuPanel-yosemite@2x.png)
   skin/classic/browser/yosemite/loop/toolbar.png            (loop/toolbar-yosemite.png)
   skin/classic/browser/yosemite/loop/toolbar@2x.png         (loop/toolbar-yosemite@2x.png)
 * skin/classic/browser/controlcenter/panel.css        (controlcenter/panel.css)
   skin/classic/browser/controlcenter/arrow-subview.svg  (../shared/controlcenter/arrow-subview.svg)
+  skin/classic/browser/controlcenter/arrow-subview-back.svg  (../shared/controlcenter/arrow-subview-back.svg)
   skin/classic/browser/controlcenter/conn-not-secure.svg  (../shared/controlcenter/conn-not-secure.svg)
   skin/classic/browser/controlcenter/conn-secure.svg  (../shared/controlcenter/conn-secure.svg)
   skin/classic/browser/controlcenter/conn-degraded.svg  (../shared/controlcenter/conn-degraded.svg)
   skin/classic/browser/controlcenter/mcb-disabled.svg  (../shared/controlcenter/mcb-disabled.svg)
   skin/classic/browser/controlcenter/permissions.svg  (../shared/controlcenter/permissions.svg)
   skin/classic/browser/controlcenter/tracking-protection.svg                 (../shared/controlcenter/tracking-protection.svg)
   skin/classic/browser/controlcenter/tracking-protection-disabled.svg        (../shared/controlcenter/tracking-protection-disabled.svg)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/controlcenter/arrow-subview-back.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <polygon fill="#fff" points="12,3.5 10.5,2 4.625,8 10.5,14 12,12.5 7.625,8" />
+</svg>
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -7,21 +7,26 @@
    connection security. Show the organization address for EV certs. */
 #identity-popup-securityView:not(.unknownIdentity):not(.verifiedIdentity):not(.mixedContent):not(.weakCipher) > #identity-popup-content-supplemental,
 /* Show the "Connection is secure" labels only for EV and DV certs. */
 #identity-popup-security-content:not(.verifiedIdentity):not(.verifiedDomain) > .identity-popup-connection-secure,
 #identity-popup-securityView:not(.verifiedIdentity):not(.verifiedDomain) > #identity-popup-securityView-header > .identity-popup-connection-secure,
 /* Show the "Connection is not secure" labels only for non-secure sites. */
 #identity-popup-security-content:not(.unknownIdentity) > .identity-popup-connection-not-secure,
 #identity-popup-securityView:not(.unknownIdentity) > #identity-popup-securityView-header > .identity-popup-connection-not-secure,
+/* Show "This page is stored on your computer" only for file URLs. */
+#identity-popup-security-content:not(.fileURI) > .identity-popup-connection-file-uri,
+#identity-popup-securityView:not(.fileURI) > #identity-popup-securityView-header > .identity-popup-connection-file-uri,
 /* Show "This is a secure internal page" only for whitelisted pages. */
 #identity-popup-securityView:not(.chromeUI) > #identity-popup-securityView-header > .identity-popup-connection-internal,
 #identity-popup-security-content:not(.chromeUI) > .identity-popup-connection-internal,
 /* Hide the subsection arrow for whitelisted chromeUI pages. */
 #identity-popup-security-content.chromeUI + .identity-popup-expander,
+/* Hide the subsection arrow for whitelisted file URI pages. */
+#identity-popup-security-content.fileURI + .identity-popup-expander,
 /* Hide the tracking protection section for whitelisted chromeUI pages. */
 #identity-popup-mainView.chromeUI > #tracking-protection-container {
   display: none;
 }
 
 /* PANEL */
 
 #identity-popup,
@@ -106,26 +111,18 @@
 
 .identity-popup-expander:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
 }
 
 .identity-popup-expander[panel-multiview-anchor] {
   transition: background-color 250ms ease-in;
   background-color: Highlight;
-  background-image: url("chrome://browser/skin/customizableui/subView-arrow-back-inverted.png"),
+  background-image: url("chrome://browser/skin/controlcenter/arrow-subview-back.svg"),
                     linear-gradient(rgba(255,255,255,0.3), transparent);
-  color: HighlightText;
-}
-
-@media (min-resolution: 1.1dppx) {
-  .identity-popup-expander[panel-multiview-anchor] {
-    background-image: url("chrome://browser/skin/customizableui/subView-arrow-back-inverted@2x.png"),
-                      linear-gradient(rgba(255,255,255,0.3), transparent);
-  }
 }
 
 .identity-popup-expander > .button-box {
   padding: 0;
   -moz-appearance: none;
   border: solid ThreeDShadow;
   border-width: 0 0 0 1px;
 }
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -197,16 +197,17 @@ browser.jar:
         skin/classic/browser/loop/toolbar-lunaSilver.png             (loop/toolbar-lunaSilver.png)
         skin/classic/browser/loop/toolbar-lunaSilver@2x.png          (loop/toolbar-lunaSilver@2x.png)
         skin/classic/browser/loop/toolbar-win8.png                   (loop/toolbar-win8.png)
         skin/classic/browser/loop/toolbar-win8@2x.png                (loop/toolbar-win8@2x.png)
         skin/classic/browser/loop/toolbar-XP.png                     (loop/toolbar-XP.png)
         skin/classic/browser/loop/toolbar-XP@2x.png                  (loop/toolbar-XP@2x.png)
 *       skin/classic/browser/controlcenter/panel.css                 (controlcenter/panel.css)
         skin/classic/browser/controlcenter/arrow-subview.svg  (../shared/controlcenter/arrow-subview.svg)
+        skin/classic/browser/controlcenter/arrow-subview-back.svg  (../shared/controlcenter/arrow-subview-back.svg)
         skin/classic/browser/controlcenter/conn-not-secure.svg  (../shared/controlcenter/conn-not-secure.svg)
         skin/classic/browser/controlcenter/conn-degraded.svg  (../shared/controlcenter/conn-degraded.svg)
         skin/classic/browser/controlcenter/conn-secure.svg  (../shared/controlcenter/conn-secure.svg)
         skin/classic/browser/controlcenter/mcb-disabled.svg  (../shared/controlcenter/mcb-disabled.svg)
         skin/classic/browser/controlcenter/permissions.svg  (../shared/controlcenter/permissions.svg)
         skin/classic/browser/controlcenter/tracking-protection.svg                 (../shared/controlcenter/tracking-protection.svg)
         skin/classic/browser/controlcenter/tracking-protection-disabled.svg        (../shared/controlcenter/tracking-protection-disabled.svg)
         skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
--- a/mobile/android/base/RestrictedProfiles.java
+++ b/mobile/android/base/RestrictedProfiles.java
@@ -32,16 +32,18 @@ public class RestrictedProfiles {
     private static final List<String> BANNED_SCHEMES = new ArrayList<String>() {{
         add("file");
         add("chrome");
         add("resource");
         add("jar");
         add("wyciwyg");
     }};
 
+    private static final String ABOUT_ADDONS = "about:addons";
+
     /**
      * This is a hack to allow non-GeckoApp activities to safely call into
      * RestrictedProfiles without reworking this class or GeckoProfile.
      *
      * It can be removed after Bug 1077590 lands.
      */
     public static void initWithProfile(GeckoProfile profile) {
         inGuest = profile.inGuestMode();
@@ -266,16 +268,23 @@ public class RestrictedProfiles {
         if (getInGuest()) {
             if (Restriction.DISALLOW_BROWSE_FILES == restriction) {
                 return canLoadUrl(context, url);
             }
 
             return !GUEST_RESTRICTIONS.contains(restriction);
         }
 
+        // Disallow browsing about:addons if 'disallow install extension' restriction is enforced
+        if (restriction == Restriction.DISALLOW_BROWSE_FILES
+            && url.toLowerCase().startsWith(ABOUT_ADDONS)
+            && !isAllowed(context, Restriction.DISALLOW_INSTALL_EXTENSION)) {
+            return false;
+        }
+
         // NOTE: Restrictions hold the opposite intention, so we need to flip it.
         return !getRestriction(context, restriction);
     }
 
     @WrapElementForJNI
     public static String getUserRestrictions() {
         return getUserRestrictions(GeckoAppShell.getContext());
     }
--- a/mobile/android/base/home/TabMenuStrip.java
+++ b/mobile/android/base/home/TabMenuStrip.java
@@ -60,17 +60,17 @@ public class TabMenuStrip extends Horizo
         shadowPaint.setStrokeWidth(0.0f);
     }
 
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
 
         final int height = getHeight();
-        canvas.drawRect(0, height - shadowSize, getWidth(), height, shadowPaint);
+        canvas.drawRect(0, height - shadowSize, layout.getWidth(), height, shadowPaint);
     }
 
     @Override
     public void onAddPagerView(String title) {
         layout.onAddPagerView(title);
     }
 
     @Override
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -561,18 +561,18 @@ just addresses the organization to follo
 <!ENTITY loaded_mixed_content_message "This page is displaying content that isn\'t secure.">
 <!ENTITY blocked_mixed_content_message_top "&brandShortName; has blocked content that isn\'t secure.">
 <!ENTITY blocked_mixed_content_message_bottom "Most websites will still work properly even when this content is blocked.">
 
 <!-- Tracking content notifications in site identity popup -->
 <!ENTITY doorhanger_tracking_title "Tracking protection">
 <!ENTITY doorhanger_tracking_state_enabled "Enabled">
 <!ENTITY doorhanger_tracking_state_disabled "Disabled">
-<!ENTITY doorhanger_tracking_message_enabled "Blocking tracking elements that may affect your browsing experience.">
-<!ENTITY doorhanger_tracking_message_disabled "Attempts to track your online behavior are not being blocked.">
+<!ENTITY doorhanger_tracking_message_enabled1 "Attempts to track your online behavior have been blocked.">
+<!ENTITY doorhanger_tracking_message_disabled1 "This site includes content that tracks your browsing.">
 
 <!-- Common mixed and tracking content strings in site identity popup -->
 <!ENTITY learn_more "Learn More">
 <!ENTITY enable_protection "Enable protection">
 <!ENTITY disable_protection "Disable protection">
 <!ENTITY keep_blocking "Keep blocking">
 
 <!ENTITY private_data_success "Private data cleared">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -467,18 +467,18 @@
   <string name="identity_encrypted">&identity_encrypted;</string>
   <string name="loaded_mixed_content_message">&loaded_mixed_content_message;</string>
   <string name="blocked_mixed_content_message_top">&blocked_mixed_content_message_top;</string>
   <string name="blocked_mixed_content_message_bottom">&blocked_mixed_content_message_bottom;</string>
 
   <string name="doorhanger_tracking_title">&doorhanger_tracking_title;</string>
   <string name="doorhanger_tracking_state_enabled">&doorhanger_tracking_state_enabled;</string>
   <string name="doorhanger_tracking_state_disabled">&doorhanger_tracking_state_disabled;</string>
-  <string name="doorhanger_tracking_message_enabled">&doorhanger_tracking_message_enabled;</string>
-  <string name="doorhanger_tracking_message_disabled">&doorhanger_tracking_message_disabled;</string>
+  <string name="doorhanger_tracking_message_enabled">&doorhanger_tracking_message_enabled1;</string>
+  <string name="doorhanger_tracking_message_disabled">&doorhanger_tracking_message_disabled1;</string>
 
   <string name="learn_more">&learn_more;</string>
   <string name="enable_protection">&enable_protection;</string>
   <string name="disable_protection">&disable_protection;</string>
   <string name="keep_blocking">&keep_blocking;</string>
 
   <!-- Clear private data -->
   <string name="private_data_success">&private_data_success;</string>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -3045,21 +3045,16 @@ var NativeWindow = {
       return this.makeURLAbsolute(aLink.baseURI, href);
     },
 
     _copyStringToDefaultClipboard: function(aString) {
       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
       clipboard.copyString(aString);
     },
 
-    _shareStringWithDefault: function(aSharedString, aTitle) {
-      let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
-      sharing.shareWithDefault(aSharedString, "text/plain", aTitle);
-    },
-
     _stripScheme: function(aString) {
       let index = aString.indexOf(":");
       return aString.slice(index + 1);
     }
   }
 };
 
 XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
@@ -6012,16 +6007,20 @@ var FormAssistant = {
   // and sends the suggestions to the Java UI, along with element position data. As
   // autocomplete queries are asynchronous, calls aCallback when done with a true
   // argument if results were found and false if no results were found.
   _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
     if (!this._isAutoComplete(aElement)) {
       aCallback(false);
       return;
     }
+    if (this._isDisabledElement(aElement)) {
+      aCallback(false);
+      return;
+    }
 
     // Don't display the form auto-complete popup after the user starts typing
     // to avoid confusing somes IME. See bug 758820 and bug 632744.
     if (this._isBlocklisted && aElement.value.length > 0) {
       aCallback(false);
       return;
     }
 
@@ -6078,16 +6077,27 @@ var FormAssistant = {
       rect: ElementTouchHelper.getBoundingContentRect(aElement)
     });
 
     return true;
   },
 
   _hideFormAssistPopup: function _hideFormAssistPopup() {
     Messaging.sendRequest({ type: "FormAssist:Hide" });
+  },
+
+  _isDisabledElement : function(aElement) {
+    let currentElement = aElement;
+    while (currentElement) {
+      if(currentElement.disabled)
+	return true;
+
+      currentElement = currentElement.parentElement;
+    }
+    return false;
   }
 };
 
 /**
  * An object to watch for Gecko status changes -- add-on installs, pref changes
  * -- and reflect them back to Java.
  */
 let HealthReportStatusListener = {
--- a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
@@ -35,16 +35,22 @@ public class TestJarReader extends Instr
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
         assertNull(stream);
 
         // Test looking for a file that doesn't exist in the APK.
         url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
         assertNull(stream);
 
+        // Test looking for a file that doesn't exist in the APK.
+        // Bug 1174922, prefixed string / length error.
+        url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD";
+        stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+        assertNull(stream);
+
         // Test looking for an jar with an invalid url.
         url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
         assertNull(stream);
 
         // Test looking for a file that doesn't exist on disk.
         url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
--- a/mobile/android/tests/browser/robocop/testJarReader.java
+++ b/mobile/android/tests/browser/robocop/testJarReader.java
@@ -11,24 +11,22 @@ import org.mozilla.gecko.util.GeckoJarRe
 
 import android.content.Context;
 
 /**
  * A basic jar reader test. Tests reading a png from fennec's apk, as well
  * as loading some invalid jar urls.
  */
 public class testJarReader extends BaseTest {
-    public void testGetJarURL() {
+    public void testJarReader() {
         // Invalid characters are escaped.
         final String s = GeckoJarReader.computeJarURI("some[1].apk", "something/else");
         mAsserter.ok(!s.contains("["), "Illegal characters are escaped away.", null);
         mAsserter.ok(!s.toLowerCase().contains("%2f"), "Path characters aren't escaped.", null);
-    }
 
-    public void testJarReader() {
         final Context context = getInstrumentation().getTargetContext().getApplicationContext();
         String appPath = getActivity().getApplication().getPackageResourcePath();
         mAsserter.isnot(appPath, null, "getPackageResourcePath is non-null");
 
         // Test reading a file from a jar url that looks correct.
         String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
         InputStream stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
         mAsserter.isnot(stream, null, "JarReader returned non-null for valid file in valid jar");
@@ -38,16 +36,22 @@ public class testJarReader extends BaseT
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
         mAsserter.is(stream, null, "JarReader returned null for non-existent file in valid jar");
 
         // Test looking for a file that doesn't exist in the APK.
         url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
         mAsserter.is(stream, null, "JarReader returned null for valid file in invalid jar file");
 
+        // Test looking for a file that doesn't exist in the APK.
+        // Bug 1174922, prefixed string / length error.
+        url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD";
+        stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+        mAsserter.is(stream, null, "JarReader returned null for valid file in other invalid jar file");
+
         // Test looking for an jar with an invalid url.
         url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
         mAsserter.is(stream, null, "JarReader returned null for bad jar url");
 
         // Test looking for a file that doesn't exist on disk.
         url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
         stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
--- a/mozglue/linker/Zip.h
+++ b/mozglue/linker/Zip.h
@@ -265,17 +265,17 @@ private:
     StringBuf(const char *buf, size_t length): buf(buf), length(length) { }
 
     /**
      * Returns whether the string has the same content as the given zero
      * terminated string.
      */
     bool Equals(const char *str) const
     {
-      return strncmp(str, buf, length) == 0;
+      return (strncmp(str, buf, length) == 0 && str[length] == '\0');
     }
 
   private:
     const char *buf;
     size_t length;
   };
 
 /* All the following types need to be packed */
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -494,85 +494,50 @@ FxAccountsInternal.prototype = {
           this.startVerifiedCheck(credentials);
         }
       }).then(() => {
         return currentAccountState.resolve();
       });
     })
   },
 
-  /**
-   * returns a promise that fires with the keypair.
-   */
-  getKeyPair: Task.async(function* (mustBeValidUntil) {
-    // If the debugging pref to ignore cached authentication credentials is set for Sync,
-    // then don't use any cached key pair, i.e., generate a new one and get it signed.
-    // The purpose of this pref is to expedite any auth errors as the result of a
-    // expired or revoked FxA session token, e.g., from resetting or changing the FxA
-    // password.
-    let ignoreCachedAuthCredentials = false;
-    try {
-      ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
-    } catch(e) {
-      // Pref doesn't exist
-    }
-    let currentState = this.currentAccountState;
-    let accountData = yield currentState.getUserAccountData("keyPair");
-    if (!ignoreCachedAuthCredentials && accountData.keyPair && (accountData.keyPair.validUntil > mustBeValidUntil)) {
-      log.debug("getKeyPair: already have a keyPair");
-      return accountData.keyPair.keyPair;
-    }
-    // Otherwse, create a keypair and set validity limit.
-    let willBeValidUntil = this.now() + KEY_LIFETIME;
-    let kp = yield new Promise((resolve, reject) => {
-      jwcrypto.generateKeyPair("DS160", (err, kp) => {
-        if (err) {
-          return reject(err);
-        }
-        log.debug("got keyPair");
-        let toUpdate = {
-          keyPair: {
-            keyPair: kp,
-            validUntil: willBeValidUntil
-          },
-          cert: null
-        };
-        currentState.updateUserAccountData(toUpdate).then(() => {
-          resolve(kp);
-        }).catch(err => {
-          log.error("Failed to update account data with keypair and cert");
-        });
-      });
-    });
-    return kp;
-  }),
 
   /**
    * returns a promise that fires with the assertion.  If there is no verified
    * signed-in user, fires with null.
    */
   getAssertion: function getAssertion(audience) {
+    return this._getAssertion(audience);
+  },
+
+  // getAssertion() is "public" so screws with our mock story. This
+  // implementation method *can* be (and is) mocked by tests.
+  _getAssertion: function _getAssertion(audience) {
     log.debug("enter getAssertion()");
     let currentState = this.currentAccountState;
-    let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
     return currentState.getUserAccountData().then(data => {
       if (!data) {
         // No signed-in user
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // Signed-in user has not verified email
         return null;
       }
-      return this.getKeyPair(mustBeValidUntil).then(keyPair => {
-        return this.getCertificate(data, keyPair, mustBeValidUntil)
-          .then(cert => {
-            return this.getAssertionFromCert(data, keyPair, cert, audience);
-          });
-      });
+      if (!data.sessionToken) {
+        // can't get a signed certificate without a session token, but that
+        // should be impossible - make log noise about it.
+        log.error("getAssertion called without a session token!");
+        return null;
+      }
+      return this.getKeypairAndCertificate(currentState).then(
+        ({keyPair, certificate}) => {
+          return this.getAssertionFromCert(data, keyPair, certificate, audience);
+        }
+      );
     }).then(result => currentState.resolve(result));
   },
 
   /**
    * Resend the verification email fot the currently signed-in user.
    *
    */
   resendVerificationEmail: function resendVerificationEmail() {
@@ -827,44 +792,101 @@ FxAccountsInternal.prototype = {
     return this.fxAccountsClient.signCertificate(
       sessionToken,
       JSON.parse(serializedPublicKey),
       lifetime
     );
   },
 
   /**
-   * returns a promise that fires with a certificate.
+   * returns a promise that fires with {keyPair, certificate}.
    */
-  getCertificate: Task.async(function* (data, keyPair, mustBeValidUntil) {
+  getKeypairAndCertificate: Task.async(function* (currentState) {
+    // If the debugging pref to ignore cached authentication credentials is set for Sync,
+    // then don't use any cached key pair/certificate, i.e., generate a new
+    // one and get it signed.
+    // The purpose of this pref is to expedite any auth errors as the result of a
+    // expired or revoked FxA session token, e.g., from resetting or changing the FxA
+    // password.
+    let ignoreCachedAuthCredentials = false;
+    try {
+      ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
+    } catch(e) {
+      // Pref doesn't exist
+    }
+    let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
+    let accountData = yield currentState.getUserAccountData(["cert", "keyPair", "sessionToken"]);
+
+    let keyPairValid = !ignoreCachedAuthCredentials &&
+                       accountData.keyPair &&
+                       (accountData.keyPair.validUntil > mustBeValidUntil);
+    let certValid = !ignoreCachedAuthCredentials &&
+                    accountData.cert &&
+                    (accountData.cert.validUntil > mustBeValidUntil);
     // TODO: get the lifetime from the cert's .exp field
-    let currentState = this.currentAccountState;
-    let accountData = yield currentState.getUserAccountData("cert");
-    if (accountData.cert && accountData.cert.validUntil > mustBeValidUntil) {
-      log.debug(" getCertificate already had one");
-      return accountData.cert.cert;
+    if (keyPairValid && certValid) {
+      log.debug("getKeypairAndCertificate: already have keyPair and certificate");
+      return {
+        keyPair: accountData.keyPair.rawKeyPair,
+        certificate: accountData.cert.rawCert
+      }
     }
+    // We are definately going to generate a new cert, either because it has
+    // already expired, or the keyPair has - and a new keyPair means we must
+    // generate a new cert.
+
+    // A keyPair has a longer lifetime than a cert, so it's possible we will
+    // have a valid keypair but an expired cert, which means we can skip
+    // keypair generation.
+    // Either way, the cert will require hitting the network, so bail now if
+    // we know that's going to fail.
     if (Services.io.offline) {
       throw new Error(ERROR_OFFLINE);
     }
-    let willBeValidUntil = this.now() + CERT_LIFETIME;
-    let cert = yield this.getCertificateSigned(data.sessionToken,
-                                               keyPair.serializedPublicKey,
-                                               CERT_LIFETIME);
-    log.debug("getCertificate got a new one: " + !!cert);
-    if (cert) {
+
+    let keyPair;
+    if (keyPairValid) {
+      keyPair = accountData.keyPair;
+    } else {
+      let keyWillBeValidUntil = this.now() + KEY_LIFETIME;
+      keyPair = yield new Promise((resolve, reject) => {
+        jwcrypto.generateKeyPair("DS160", (err, kp) => {
+          if (err) {
+            return reject(err);
+          }
+          log.debug("got keyPair");
+          resolve({
+            rawKeyPair: kp,
+            validUntil: keyWillBeValidUntil,
+          });
+        });
+      });
+    }
+
+    // and generate the cert.
+    let certWillBeValidUntil = this.now() + CERT_LIFETIME;
+    let certificate = yield this.getCertificateSigned(accountData.sessionToken,
+                                                      keyPair.rawKeyPair.serializedPublicKey,
+                                                      CERT_LIFETIME);
+    log.debug("getCertificate got a new one: " + !!certificate);
+    if (certificate) {
+      // Cache both keypair and cert.
       let toUpdate = {
+        keyPair,
         cert: {
-          cert: cert,
-          validUntil: willBeValidUntil
-        }
+          rawCert: certificate,
+          validUntil: certWillBeValidUntil,
+        },
       };
       yield currentState.updateUserAccountData(toUpdate);
     }
-    return cert;
+    return {
+      keyPair: keyPair.rawKeyPair,
+      certificate: certificate,
+    }
   }),
 
   getUserAccountData: function() {
     return this.currentAccountState.getUserAccountData();
   },
 
   isUserEmailVerified: function isUserEmailVerified(data) {
     return !!(data && data.verified);
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -161,16 +161,32 @@ function MockFxAccounts() {
       _("mock getCertificateSigned\n");
       this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
       return this._d_signCertificate.promise;
     },
     fxAccountsClient: new MockFxAccountsClient()
   });
 }
 
+/*
+ * Some tests want a "real" fxa instance - however, we still mock the storage
+ * to keep the tests fast on b2g.
+ */
+function MakeFxAccounts(internal = {}) {
+  if (!internal.newAccountState) {
+    // we use a real accountState but mocked storage.
+    internal.newAccountState = function(credentials) {
+      let storage = new MockStorageManager();
+      storage.initialize(credentials);
+      return new AccountState(storage);
+    };
+  }
+  return new FxAccounts(internal);
+}
+
 add_test(function test_non_https_remote_server_uri_with_requireHttps_false() {
   Services.prefs.setBoolPref(
     "identity.fxaccounts.allowHttp",
     true);
   Services.prefs.setCharPref(
     "identity.fxaccounts.remote.signup.uri",
     "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
   do_check_eq(fxAccounts.getAccountsSignUpURI(),
@@ -190,26 +206,18 @@ add_test(function test_non_https_remote_
   }, "Firefox Accounts server must use HTTPS");
 
   Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
 
   run_next_test();
 });
 
 add_task(function test_get_signed_in_user_initially_unset() {
-  // This test, unlike many of the the rest, uses a (largely) un-mocked
-  // FxAccounts instance.
-  let account = new FxAccounts({
-    newAccountState(credentials) {
-      // we use a real accountState but mocked storage.
-      let storage = new MockStorageManager();
-      storage.initialize(credentials);
-      return new AccountState(storage);
-    },
-  });
+  _("Check getSignedInUser initially and after signout reports no user");
+  let account = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kA: "beef",
     kB: "cafe",
     verified: true
@@ -236,60 +244,135 @@ add_task(function test_get_signed_in_use
   let localOnly = true;
   yield account.signOut(localOnly);
 
   // user should be undefined after sign out
   result = yield account.getSignedInUser();
   do_check_eq(result, null);
 });
 
-add_task(function* test_getCertificate() {
-  _("getCertificate()");
-  // This test, unlike many of the the rest, uses a (largely) un-mocked
-  // FxAccounts instance.
-  // We do mock the storage to keep the test fast on b2g.
-  let fxa = new FxAccounts({
-    newAccountState(credentials) {
-      // we use a real accountState but mocked storage.
-      let storage = new MockStorageManager();
-      storage.initialize(credentials);
-      return new AccountState(storage);
-    },
-  });
+add_task(function* test_getCertificateOffline() {
+  _("getCertificateOffline()");
+  let fxa = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
-    assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
-    verified: true
+    verified: true,
   };
+
   yield fxa.setSignedInUser(credentials);
 
   // Test that an expired cert throws if we're offline.
-  fxa.internal.currentAccountState.cert = {
-    validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT")
-  };
   let offline = Services.io.offline;
   Services.io.offline = true;
-  // This call would break from missing parameters ...
-  yield fxa.internal.getCertificate().then(
+  yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState).then(
     result => {
       Services.io.offline = offline;
       do_throw("Unexpected success");
     },
     err => {
       Services.io.offline = offline;
       // ... so we have to check the error string.
       do_check_eq(err, "Error: OFFLINE");
     }
   );
+  yield fxa.signOut(/*localOnly = */true);
+});
+
+add_task(function* test_getCertificateCached() {
+  _("getCertificateCached()");
+  let fxa = MakeFxAccounts();
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    sessionToken: "dead",
+    verified: true,
+    // A cached keypair and cert that remain valid.
+    keyPair: {
+      validUntil: Date.now() + KEY_LIFETIME + 10000,
+      rawKeyPair: "good-keypair",
+    },
+    cert: {
+      validUntil: Date.now() + CERT_LIFETIME + 10000,
+      rawCert: "good-cert",
+    },
+  };
+
+  yield fxa.setSignedInUser(credentials);
+  let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState);
+  // should have the same keypair and cert.
+  do_check_eq(keyPair, credentials.keyPair.rawKeyPair);
+  do_check_eq(certificate, credentials.cert.rawCert);
+  yield fxa.signOut(/*localOnly = */true);
 });
 
+add_task(function* test_getCertificateExpiredCert() {
+  _("getCertificateExpiredCert()");
+  let fxa = MakeFxAccounts({
+    getCertificateSigned() {
+      return "new cert";
+    }
+  });
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    sessionToken: "dead",
+    verified: true,
+    // A cached keypair that remains valid.
+    keyPair: {
+      validUntil: Date.now() + KEY_LIFETIME + 10000,
+      rawKeyPair: "good-keypair",
+    },
+    // A cached certificate which has expired.
+    cert: {
+      validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"),
+      rawCert: "expired-cert",
+    },
+  };
+  yield fxa.setSignedInUser(credentials);
+  let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState);
+  // should have the same keypair but a new cert.
+  do_check_eq(keyPair, credentials.keyPair.rawKeyPair);
+  do_check_neq(certificate, credentials.cert.rawCert);
+  yield fxa.signOut(/*localOnly = */true);
+});
+
+add_task(function* test_getCertificateExpiredKeypair() {
+  _("getCertificateExpiredKeypair()");
+  let fxa = MakeFxAccounts({
+    getCertificateSigned() {
+      return "new cert";
+    },
+  });
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    sessionToken: "dead",
+    verified: true,
+    // A cached keypair that has expired.
+    keyPair: {
+      validUntil: Date.now() - 1000,
+      rawKeyPair: "expired-keypair",
+    },
+    // A cached certificate which remains valid.
+    cert: {
+      validUntil: Date.now() + CERT_LIFETIME + 10000,
+      rawCert: "expired-cert",
+    },
+  };
+
+  yield fxa.setSignedInUser(credentials);
+  let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState);
+  // even though the cert was valid, the fact the keypair was not means we
+  // should have fetched both.
+  do_check_neq(keyPair, credentials.keyPair.rawKeyPair);
+  do_check_neq(certificate, credentials.cert.rawCert);
+  yield fxa.signOut(/*localOnly = */true);
+});
 
 // Sanity-check that our mocked client is working correctly
 add_test(function test_client_mock() {
   let fxa = new MockFxAccounts();
   let client = fxa.internal.fxAccountsClient;
   do_check_eq(client._verified, false);
   do_check_eq(typeof client.signIn, "function");
 
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -179,24 +179,20 @@ this.configureFxAccountIdentity = functi
       if (credentials) {
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(config.fxaccount.user);
       let accountState = new AccountState(storageManager);
       return accountState;
     },
-    getCertificate(data, keyPair, mustBeValidUntil) {
-      let cert = {
-        validUntil: this.now() + CERT_LIFETIME,
-        cert: "certificate",
-      };
-      this.currentAccountState.updateUserAccountData({cert: cert});
-      return Promise.resolve(cert.cert);
+    _getAssertion(audience) {
+      return Promise.resolve("assertion");
     },
+
   };
   fxa = new FxAccounts(MockInternal);
 
   let mockTSC = { // TokenServerClient
     getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
       config.fxaccount.token.uid = config.username;
       cb(null, config.fxaccount.token);
     },
--- a/toolkit/components/passwordmgr/content/passwordManager.js
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -73,28 +73,58 @@ var signonsTreeView = {
         time = new Date(signon.timePasswordChanged);
         return dateFormatter.format(time);
       case "timesUsedCol":
         return signon.timesUsed;
       default:
         return "";
     }
   },
+  isEditable : function(row, col) {
+    if (col.id == "userCol" || col.id == "passwordCol") {
+      return true;
+    }
+    return false;
+  },
   isSeparator : function(index) { return false; },
   isSorted : function() { return false; },
   isContainer : function(index) { return false; },
   cycleHeader : function(column) {},
   getRowProperties : function(row) { return ""; },
   getColumnProperties : function(column) { return ""; },
   getCellProperties : function(row,column) {
     if (column.element.getAttribute("id") == "siteCol")
       return "ltr";
 
     return "";
-  }
+  },
+  setCellText : function(row, col, value) {
+    // If there is a filter, _filterSet needs to be used, otherwise signons is used.
+    let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+    function _editLogin(field) {
+      if (value == table[row][field]) {
+        return;
+      }
+      let existingLogin = table[row].clone();
+      table[row][field] = value;
+      table[row].timePasswordChanged = Date.now();
+      passwordmanager.modifyLogin(existingLogin, table[row]);
+      signonsTree.treeBoxObject.invalidateRow(row);
+    }
+
+    if (col.id == "userCol") {
+     _editLogin("username");
+
+    } else if (col.id == "passwordCol") {
+      if (!value) {
+        return;
+      }
+      _editLogin("password");
+    }
+  },
 };
 
 
 function LoadSignons() {
   // loads signons into table
   try {
     signons = passwordmanager.getAllLogins();
   } catch (e) {
@@ -202,21 +232,25 @@ function FinalizeSignonDeletions(syncNee
     } catch (e) {
       signons = [];
     }
   }
   deletedSignons.length = 0;
 }
 
 function HandleSignonKeyPress(e) {
+  // If editing is currently performed, don't do anything.
+  if (signonsTree.getAttribute("editing")) {
+    return;
+  }
   if (e.keyCode == KeyEvent.DOM_VK_DELETE
 #ifdef XP_MACOSX
       || e.keyCode == KeyEvent.DOM_VK_BACK_SPACE
 #endif
-     ) {
+   ) {
     DeleteSignon();
   }
 }
 
 function getColumnByName(column) {
   switch (column) {
     case "hostname":
       return document.getElementById("siteCol");
@@ -407,8 +441,16 @@ function masterPasswordLogin(noPasswordC
                         // clicking 'Cancel' or entering the correct password.
   } catch (e) {
     // An exception will be thrown if the user cancels the login prompt dialog.
     // User is also logged out of Software Security Device.
   }
 
   return token.isLoggedIn();
 }
+
+function escapeKeyHandler() {
+  // If editing is currently performed, don't do anything.
+  if (signonsTree.getAttribute("editing")) {
+    return;
+  }
+  window.close();
+}
--- a/toolkit/components/passwordmgr/content/passwordManager.xul
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -19,17 +19,17 @@
   <script type="application/javascript" src="chrome://passwordmgr/content/passwordManagerCommon.js"/>
   <script type="application/javascript" src="chrome://passwordmgr/content/passwordManager.js"/>
 
   <stringbundle id="signonBundle"
                 src="chrome://passwordmgr/locale/passwordmgr.properties"/>
 
   <keyset>
     <key keycode="VK_ESCAPE" oncommand="window.close();"/>
-    <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
+    <key key="&windowClose.key;" modifiers="accel" oncommand="escapeKeyHandler();"/>
     <key key="&focusSearch1.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
     <key key="&focusSearch2.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
   </keyset>
 
   <popupset id="signonsTreeContextSet">
     <menupopup id="signonsTreeContextMenu"
            onpopupshowing="UpdateCopyPassword()">
       <menuitem id="context-copyusername"
@@ -55,16 +55,17 @@
 
     <label control="signonsTree" id="signonsIntro"/>
     <separator class="thin"/>
     <tree id="signonsTree" flex="1"
           width="750"
           style="height: 20em;"
           onkeypress="HandleSignonKeyPress(event)"
           onselect="SignonSelected();"
+          editable="true"
           context="signonsTreeContextMenu">
       <treecols>
         <treecol id="siteCol" label="&treehead.site.label;" flex="40"
                  data-field-name="hostname" persist="width"
                  ignoreincolumnpicker="true"
                  sortDirection="ascending"/>
         <splitter class="tree-splitter"/>
         <treecol id="userCol" label="&treehead.username.label;" flex="25"
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -3,14 +3,16 @@ support-files =
   authenticate.sjs
   form_basic.html
 
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 [browser_filldoorhanger.js]
 [browser_notifications.js]
 skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
+[browser_passwordmgr_editing.js]
+skip-if = os == "linux"
 [browser_passwordmgr_fields.js]
 [browser_passwordmgr_observers.js]
 [browser_passwordmgr_sort.js]
 [browser_passwordmgr_switchtab.js]
 [browser_passwordmgrcopypwd.js]
 [browser_passwordmgrdlg.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
@@ -0,0 +1,126 @@
+const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+const TIME_INTERVAL = 500;
+const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+
+let doc;
+let pwmgr;
+let pwmgrdlg;
+let signonsTree;
+
+function addLogin(site, username, password) {
+  let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                               Ci.nsILoginInfo, "init");
+  let login = new nsLoginInfo(site, site, null, username, password, "u", "p");
+  Services.logins.addLogin(login);
+}
+
+function getUsername(row) {
+  return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("userCol"));
+}
+
+function getPassword(row) {
+  return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("passwordCol"));
+}
+
+function synthesizeDblClickOnCell(aTree, column, row) {
+  let tbo = aTree.treeBoxObject;
+  let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text");
+  let x = rect.x + rect.width / 2;
+  let y = rect.y + rect.height / 2;
+  // Simulate the double click.
+  EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 },
+                             aTree.ownerDocument.defaultView);
+}
+
+function togglePasswords() {
+  pwmgrdlg.document.querySelector("#togglePasswords").doCommand();
+}
+
+function* editUsernamePromises(site, oldUsername, newUsername) {
+  is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+  let login = Services.logins.findLogins({}, site, "", "")[0];
+  is(login.username, oldUsername, "Correct username saved");
+  is(getUsername(0), oldUsername, "Correct username shown");
+  synthesizeDblClickOnCell(signonsTree, 1, 0);
+  yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+                                          "Waiting for editing");
+
+  EventUtils.sendString(newUsername, pwmgrdlg);
+  let signonsIntro = doc.querySelector("#signonsIntro");
+  EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+  yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+                                          "Waiting for editing to stop");
+
+  is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+  login = Services.logins.findLogins({}, site, "", "")[0];
+  is(login.username, newUsername, "Correct username updated");
+  is(getUsername(0), newUsername, "Correct username shown");
+}
+
+function* editPasswordPromises(site, oldPassword, newPassword) {
+  is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+  let login = Services.logins.findLogins({}, site, "", "")[0];
+  is(login.password, oldPassword, "Correct password saved");
+  is(getPassword(0), oldPassword, "Correct password shown");
+
+  synthesizeDblClickOnCell(signonsTree, 2, 0);
+  yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+                                          "Waiting for editing");
+
+  EventUtils.sendString(newPassword, pwmgrdlg);
+  let signonsIntro = doc.querySelector("#signonsIntro");
+  EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+  yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+                                          "Waiting for editing to stop");
+
+  is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+  login = Services.logins.findLogins({}, site, "", "")[0];
+  is(login.password, newPassword, "Correct password updated");
+  is(getPassword(0), newPassword, "Correct password shown");
+}
+
+add_task(function* test_setup() {
+  registerCleanupFunction(function() {
+    Services.logins.removeAllLogins();
+  });
+
+  Services.logins.removeAllLogins();
+  // Open the password manager dialog.
+  pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+
+  Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+    if (aTopic == "domwindowopened") {
+      let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+      SimpleTest.waitForFocus(function() {
+        EventUtils.sendKey("RETURN", win);
+      }, win);
+    } else if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") {
+      // Unregister ourself.
+      Services.ww.unregisterNotification(arguments.callee);
+    }
+  });
+
+  yield new Promise((resolve) => {
+    SimpleTest.waitForFocus(() => {
+      doc = pwmgrdlg.document;
+      signonsTree = doc.querySelector("#signonsTree");
+      resolve();
+    }, pwmgrdlg);
+  });
+});
+
+add_task(function* test_edit_multiple_logins() {
+  function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) {
+    addLogin(site, oldUsername, oldPassword);
+    yield* editUsernamePromises(site, oldUsername, newUsername);
+    togglePasswords();
+    yield* editPasswordPromises(site, oldPassword, newPassword);
+    togglePasswords();
+  }
+
+  yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC");
+  yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB");
+  yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA");
+
+  pwmgrdlg.close();
+});
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -157,21 +157,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://services-common/utils.js");
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
+function getMsSinceProcessStart() {
+  try {
+    return Telemetry.msSinceProcessStart();
+  } catch (ex) {
+    // If this fails return a special value.
+    return -1;
+  }
+}
+
 /**
  * This is a policy object used to override behavior for testing.
  */
 let Policy = {
   now: () => new Date(),
+  monotonicNow: getMsSinceProcessStart,
   generateSessionUUID: () => generateUUID(),
   generateSubsessionUUID: () => generateUUID(),
   setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
   clearSchedulerTickTimeout: id => clearTimeout(id),
 };
 
 /**
  * Get the ping type based on the payload.
@@ -705,16 +715,17 @@ this.TelemetrySession = Object.freeze({
   reset: function() {
     Impl._sessionId = null;
     Impl._subsessionId = null;
     Impl._previousSessionId = null;
     Impl._previousSubsessionId = null;
     Impl._subsessionCounter = 0;
     Impl._profileSubsessionCounter = 0;
     Impl._subsessionStartActiveTicks = 0;
+    Impl._subsessionStartTimeMonotonic = 0;
     this.uninstall();
     return this.setup();
   },
   /**
    * Used only for testing purposes.
    * @param {Boolean} [aForceSavePending=true] If true, always saves the ping whether Telemetry
    *        can send pings or not, which is used for testing.
    */
@@ -790,16 +801,19 @@ let Impl = {
   // null on first run.
   _previousSubsessionId: null,
   // The running no. of subsessions since the start of the browser session
   _subsessionCounter: 0,
   // The running no. of all subsessions for the whole profile life time
   _profileSubsessionCounter: 0,
   // Date of the last session split
   _subsessionStartDate: null,
+  // Start time of the current subsession using a monotonic clock for the subsession
+  // length measurements.
+  _subsessionStartTimeMonotonic: 0,
   // The active ticks counted when the subsession starts
   _subsessionStartActiveTicks: 0,
   // A task performing delayed initialization of the chrome process
   _delayedInitTask: null,
   // The deferred promise resolved when the initialization task completes.
   _delayedInitTaskDeferred: null,
   // Used to serialize session state writes to disk.
   _stateSaveSerializer: new SaveSerializer(),
@@ -1074,21 +1088,19 @@ let Impl = {
    * @param  reason
    *         The reason for the telemetry ping, this will be included in the
    *         returned metadata,
    * @return The metadata as a JS object
    */
   getMetadata: function getMetadata(reason) {
     this._log.trace("getMetadata - Reason " + reason);
 
-    let sessionStartDate = toLocalTimeISOString(Utils.truncateToDays(this._sessionStartDate));
-    let subsessionStartDate = toLocalTimeISOString(Utils.truncateToDays(this._subsessionStartDate));
-    // Compute the subsession length in milliseconds, then convert to seconds.
-    let subsessionLength =
-      Math.floor((Policy.now() - this._subsessionStartDate.getTime()) / 1000);
+    const sessionStartDate = toLocalTimeISOString(Utils.truncateToDays(this._sessionStartDate));
+    const subsessionStartDate = toLocalTimeISOString(Utils.truncateToDays(this._subsessionStartDate));
+    const monotonicNow = Policy.monotonicNow();
 
     let ret = {
       reason: reason,
       revision: HISTOGRAMS_FILE_VERSION,
       asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false),
 
       // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of
       // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here.
@@ -1100,17 +1112,23 @@ let Impl = {
       previousSessionId: this._previousSessionId,
       previousSubsessionId: this._previousSubsessionId,
 
       subsessionCounter: this._subsessionCounter,
       profileSubsessionCounter: this._profileSubsessionCounter,
 
       sessionStartDate: sessionStartDate,
       subsessionStartDate: subsessionStartDate,
-      subsessionLength: subsessionLength,
+
+      // Compute the session and subsession length in seconds.
+      // We use monotonic clocks as Date() is affected by jumping clocks (leading
+      // to negative lengths and other issues).
+      sessionLength: Math.floor(monotonicNow / 1000),
+      subsessionLength:
+        Math.floor((monotonicNow - this._subsessionStartTimeMonotonic) / 1000),
     };
 
     // TODO: Remove this when bug 1124128 lands.
     if (this._addons)
       ret.addons = this._addons;
 
     // TODO: Remove this when bug 1124128 lands.
     let flashVersion = this.getFlashVersion();
@@ -1329,16 +1347,17 @@ let Impl = {
     return payloadObj;
   },
 
   /**
    * Start a new subsession.
    */
   startNewSubsession: function () {
     this._subsessionStartDate = Policy.now();
+    this._subsessionStartTimeMonotonic = Policy.monotonicNow();
     this._previousSubsessionId = this._subsessionId;
     this._subsessionId = Policy.generateSubsessionUUID();
     this._subsessionCounter++;
     this._profileSubsessionCounter++;
   },
 
   getSessionPayload: function getSessionPayload(reason, clearSubsession) {
     this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
--- a/toolkit/components/telemetry/docs/main-ping.rst
+++ b/toolkit/components/telemetry/docs/main-ping.rst
@@ -34,17 +34,18 @@ Structure::
         previousSubsessionId: <uuid>, // subsession id of the previous subsession (even if it was in a different session),
                                       // null on first run.
 
         subsessionCounter: <number>, // the running no. of this subsession since the start of the browser session
         profileSubsessionCounter: <number>, // the running no. of all subsessions for the whole profile life time
 
         sessionStartDate: <ISO date>, // daily precision
         subsessionStartDate: <ISO date>, // daily precision, ISO date in local time
-        subsessionLength: <number>, // the subsession length in seconds
+        sessionLength: <number>, // the session length until now in seconds, monotonic
+        subsessionLength: <number>, // the subsession length in seconds, monotonic
       },
 
       childPayloads: {...}, // only present with e10s; a reduced payload from content processes
 
       simpleMeasurements: { ... },
       histograms: {},
       keyedHistograms: {},
       chromeHangs: {},
@@ -53,8 +54,26 @@ Structure::
       fileIOReports: {...},
       lateWrites: {...},
       addonDetails: { ... },
       addonHistograms: {...},
       UIMeasurements: {...},
       slowSQL: {...},
       slowSQLstartup: {...},
     }
+
+info
+----
+
+sessionLength
+~~~~~~~~~~~~~
+The length of the current session so far in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that
+are not monotonic like calculations based on ``Date.now()``.
+
+If the monotonic clock failed, this will be ``-1``.
+
+subsessionLength
+~~~~~~~~~~~~~~~~
+The length of this subsession in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that are not monotonic (e.g. based on Date.now()).
+
+If ``sessionLength`` is ``-1``, the monotonic clock is not working.
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -243,16 +243,22 @@ function fakeNow(...args) {
 
   for (let m of modules) {
     m.Policy.now = () => date;
   }
 
   return new Date(date);
 }
 
+function fakeMonotonicNow(ms) {
+  const m = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+  m.Policy.monotonicNow = () => ms;
+  return ms;
+}
+
 // Fake the timeout functions for TelemetryController sending.
 function fakePingSendTimer(set, clear) {
   let module = Cu.import("resource://gre/modules/TelemetrySend.jsm");
   let obj = Cu.cloneInto({set, clear}, module, {cloneFunctions:true});
   module.Policy.setSchedulerTickTimeout = obj.set;
   module.Policy.clearSchedulerTickTimeout = obj.clear;
 }
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -514,26 +514,28 @@ add_task(function* test_simplePing() {
   yield clearPendingPings();
   yield TelemetrySend.reset();
   PingServer.start();
   Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port);
 
   let now = new Date(2020, 1, 1, 12, 0, 0);
   let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
   fakeNow(now);
+  const monotonicStart = fakeMonotonicNow(5000);
 
   const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
   const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
   fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
   yield TelemetrySession.reset();
 
   // Session and subsession start dates are faked during TelemetrySession setup. We can
   // now fake the session duration.
   const SESSION_DURATION_IN_MINUTES = 15;
   fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
+  fakeMonotonicNow(monotonicStart + SESSION_DURATION_IN_MINUTES * 60 * 1000);
 
   yield sendPing();
   let ping = yield PingServer.promiseNextPing();
 
   checkPingFormat(ping, PING_TYPE_MAIN, true, true);
 
   // Check that we get the data we expect.
   let payload = ping.payload;
--- a/toolkit/content/contentAreaUtils.js
+++ b/toolkit/content/contentAreaUtils.js
@@ -75,25 +75,25 @@ function forbidCPOW(arg, func, argname)
 // - An image with an extension (e.g. .jpg) in its file name, using
 //   Context->Save Image As...
 // - An image without an extension (e.g. a banner ad on cnn.com) using
 //   the above method.
 // - A linked document using Save Link As...
 // - A linked document using Alt-click Save Link As...
 //
 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
-                 aSkipPrompt, aReferrer, aSourceDocument)
+                 aSkipPrompt, aReferrer, aSourceDocument, aIsContentWindowPrivate)
 {
   forbidCPOW(aURL, "saveURL", "aURL");
   forbidCPOW(aReferrer, "saveURL", "aReferrer");
   // Allow aSourceDocument to be a CPOW.
 
   internalSave(aURL, null, aFileName, null, null, aShouldBypassCache,
                aFilePickerTitleKey, null, aReferrer, aSourceDocument,
-               aSkipPrompt, null);
+               aSkipPrompt, null, aIsContentWindowPrivate);
 }
 
 // Just like saveURL, but will get some info off the image before
 // calling internalSave
 // Clientele: (Make sure you don't break any of these)
 //  - Context ->  Save Image As...
 const imgICache = Components.interfaces.imgICache;
 const nsISupportsCString = Components.interfaces.nsISupportsCString;
@@ -259,29 +259,34 @@ const kSaveAsType_Text     = 2; // Save 
  *        Alternate title for the file picker
  * @param aChosenData
  *        If non-null this contains an instance of object AutoChosen (see below)
  *        which holds pre-determined data so that the user does not need to be
  *        prompted for a target filename.
  * @param aReferrer
  *        the referrer URI object (not URL string) to use, or null
  *        if no referrer should be sent.
- * @param aInitiatingDocument
+ * @param aInitiatingDocument [optional]
  *        The document from which the save was initiated.
+ *        If this is omitted then aIsContentWindowPrivate has to be provided.
  * @param aSkipPrompt [optional]
  *        If set to true, we will attempt to save the file to the
  *        default downloads folder without prompting.
  * @param aCacheKey [optional]
  *        If set will be passed to saveURI.  See nsIWebBrowserPersist for
  *        allowed values.
+ * @param aIsContentWindowPrivate [optional]
+ *        This parameter is provided when the aInitiatingDocument is not a
+ *        real document object. Stores whether aInitiatingDocument.defaultView
+ *        was private or not.
  */
 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
                       aContentType, aShouldBypassCache, aFilePickerTitleKey,
                       aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt,
-                      aCacheKey)
+                      aCacheKey, aIsContentWindowPrivate)
 {
   forbidCPOW(aURL, "internalSave", "aURL");
   forbidCPOW(aReferrer, "internalSave", "aReferrer");
   forbidCPOW(aCacheKey, "internalSave", "aCacheKey");
   // Allow aInitiatingDocument to be a CPOW.
 
   if (aSkipPrompt == undefined)
     aSkipPrompt = false;
@@ -352,17 +357,18 @@ function internalSave(aURL, aDocument, a
       sourceURI         : sourceURI,
       sourceReferrer    : aReferrer,
       sourceDocument    : useSaveDocument ? aDocument : null,
       targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null,
       targetFile        : file,
       sourceCacheKey    : aCacheKey,
       sourcePostData    : nonCPOWDocument ? getPostData(aDocument) : null,
       bypassCache       : aShouldBypassCache,
-      initiatingWindow  : aInitiatingDocument.defaultView
+      initiatingWindow  : aInitiatingDocument && aInitiatingDocument.defaultView,
+      isContentWindowPrivate : aIsContentWindowPrivate
     };
 
     // Start the actual save process
     internalPersist(persistArgs);
   }
 }
 
 /**
@@ -387,18 +393,22 @@ function internalSave(aURL, aDocument, a
  *        The nsIFile of the file to create
  * @param persistArgs.targetContentType
  *        Required and used only when persistArgs.sourceDocument is present,
  *        determines the final content type of the saved file, or null to use
  *        the same content type as the source document. Currently only
  *        "text/plain" is meaningful.
  * @param persistArgs.bypassCache
  *        If true, the document will always be refetched from the server
- * @param persistArgs.initiatingWindow
+ * @param persistArgs.initiatingWindow [optional]
  *        The window from which the save operation was initiated.
+ *        If this is omitted then isContentWindowPrivate has to be provided.
+ * @param persistArgs.isContentWindowPrivate [optional]
+ *        If present then isPrivate is set to this value without using
+ *        persistArgs.initiatingWindow.
  */
 function internalPersist(persistArgs)
 {
   var persist = makeWebBrowserPersist();
 
   // Calculate persist flags.
   const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
   const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
@@ -409,17 +419,20 @@ function internalPersist(persistArgs)
     persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
 
   // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
   persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
 
   // Find the URI associated with the target file
   var targetFileURL = makeFileURI(persistArgs.targetFile);
 
-  let isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(persistArgs.initiatingWindow);
+  let isPrivate = persistArgs.isContentWindowPrivate;
+  if (isPrivate === undefined) {
+    isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(persistArgs.initiatingWindow);
+  }
 
   // Create download and initiate it (below)
   var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer);
   tr.init(persistArgs.sourceURI,
           targetFileURL, "", null, null, null, persist, isPrivate);
   persist.progressListener = new DownloadListener(window, tr);
 
   if (persistArgs.sourceDocument) {
--- a/toolkit/devtools/output-parser.js
+++ b/toolkit/devtools/output-parser.js
@@ -39,22 +39,23 @@ loader.lazyGetter(this, "DOMUtils", func
  * adding doorhanger previews where possible (images, angles, lengths,
  * border radius, cubic-bezier etc.).
  *
  * Usage:
  *   const {devtools} =
  *      Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
  *   const {OutputParser} = devtools.require("devtools/output-parser");
  *
- *   let parser = new OutputParser();
+ *   let parser = new OutputParser(document);
  *
  *   parser.parseCssProperty("color", "red"); // Returns document fragment.
  */
-function OutputParser() {
+function OutputParser(document) {
   this.parsed = [];
+  this.doc = document;
   this.colorSwatches = new WeakMap();
   this._onSwatchMouseDown = this._onSwatchMouseDown.bind(this);
 }
 
 exports.OutputParser = OutputParser;
 
 OutputParser.prototype = {
   /**
@@ -437,32 +438,30 @@ OutputParser.prototype = {
    *
    * @param  {String} tagName
    *         Tag type e.g. "div"
    * @param  {Object} attributes
    *         e.g. {class: "someClass", style: "cursor:pointer"};
    * @param  {String} [value]
    *         If a value is included it will be appended as a text node inside
    *         the tag. This is useful e.g. for span tags.
-   * @return {Node} Newly created Node.
+   * @return {Node} Newly created Node.
    */
   _createNode: function(tagName, attributes, value="") {
-    let win = Services.appShell.hiddenDOMWindow;
-    let doc = win.document;
-    let node = doc.createElementNS(HTML_NS, tagName);
+    let node = this.doc.createElementNS(HTML_NS, tagName);
     let attrs = Object.getOwnPropertyNames(attributes);
 
     for (let attr of attrs) {
       if (attributes[attr]) {
         node.setAttribute(attr, attributes[attr]);
       }
     }
 
     if (value) {
-      let textNode = doc.createTextNode(value);
+      let textNode = this.doc.createTextNode(value);
       node.appendChild(textNode);
     }
 
     return node;
   },
 
   /**
    * Append a node to the output.
@@ -498,23 +497,21 @@ OutputParser.prototype = {
 
   /**
    * Take all output and append it into a single DocumentFragment.
    *
    * @return {DocumentFragment}
    *         Document Fragment
    */
   _toDOM: function() {
-    let win = Services.appShell.hiddenDOMWindow;
-    let doc = win.document;
-    let frag = doc.createDocumentFragment();
+    let frag = this.doc.createDocumentFragment();
 
     for (let item of this.parsed) {
       if (typeof item === "string") {
-        frag.appendChild(doc.createTextNode(item));
+        frag.appendChild(this.doc.createTextNode(item));
       } else {
         frag.appendChild(item);
       }
     }
 
     this.parsed.length = 0;
     return frag;
   },