Bug 947212 - Broadcast form data and move it out of tabData.entries[] r=yoric
authorTim Taubert <ttaubert@mozilla.com>
Tue, 03 Dec 2013 18:56:33 +0100
changeset 163991 14a4f50a46811dfa1f70298407cac23ae1012c14
parent 163990 cf3f073c8b4bc54f4c0ecd94f4b9cecc0290199b
child 163992 35b6230f10c1229d25b35886e801eaa10dd08c17
push id26022
push userryanvm@gmail.com
push dateFri, 17 Jan 2014 19:56:22 +0000
treeherdermozilla-central@fad7172d4542 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyoric
bugs947212
milestone29.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
Bug 947212 - Broadcast form data and move it out of tabData.entries[] r=yoric From 3111a6c4272a1e058db6a88e02f8688f8c49cc5f Mon Sep 17 00:00:00 2001
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/src/ContentRestore.jsm
browser/components/sessionstore/src/DocumentUtils.jsm
browser/components/sessionstore/src/FormData.jsm
browser/components/sessionstore/src/PrivacyLevelFilter.jsm
browser/components/sessionstore/src/SessionStore.jsm
browser/components/sessionstore/src/TabState.jsm
browser/components/sessionstore/src/TextAndScrollData.jsm
browser/components/sessionstore/src/moz.build
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -13,26 +13,26 @@ let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Timer.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+  "resource:///modules/sessionstore/FormData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource:///modules/sessionstore/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
-  "resource:///modules/sessionstore/TextAndScrollData.jsm");
 
 Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
 let gFrameTree = new FrameTree(this);
 
 Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
 XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
                             () => { return new ContentRestore(this) });
 
@@ -69,22 +69,19 @@ function isSessionStorageEvent(event) {
 }
 
 /**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
-  DOM_EVENTS: [
-    "load", "pageshow", "change", "input"
-  ],
-
   init: function () {
-    this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
+    addEventListener("load", this, true);
+    addEventListener("pageshow", this, true);
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "load":
         // Ignore load events from subframes.
         if (event.target == content.document) {
           // If we're in the process of restoring, this load may signal
@@ -101,20 +98,16 @@ let EventListener = {
           // Send a load message for all loads so we can invalidate the TabStateCache.
           sendAsyncMessage("SessionStore:load");
         }
         break;
       case "pageshow":
         if (event.persisted && event.target == content.document)
           sendAsyncMessage("SessionStore:pageshow");
         break;
-      case "input":
-      case "change":
-        sendAsyncMessage("SessionStore:input");
-        break;
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
 
 /**
@@ -134,24 +127,16 @@ let MessageListener = {
     this.MESSAGES.forEach(m => addMessageListener(m, this));
   },
 
   receiveMessage: function ({name, data}) {
     let id = data ? data.id : 0;
     switch (name) {
       case "SessionStore:collectSessionHistory":
         let history = SessionHistory.collect(docShell);
-        if ("index" in history) {
-          let tabIndex = history.index - 1;
-          // Don't include private data. It's only needed when duplicating
-          // tabs, which collects data synchronously.
-          TextAndScrollData.updateFrame(history.entries[tabIndex],
-                                        content,
-                                        docShell.isAppTab);
-        }
         sendAsyncMessage(name, {id: id, data: history});
         break;
       case "SessionStore:restoreHistory":
         let reloadCallback = () => {
           // Inform SessionStore.jsm about the reload. It will send
           // restoreTabContent in response.
           sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch});
         };
@@ -207,25 +192,17 @@ let SyncHandler = {
     // Send this object as a CPOW to chrome. In single-process mode,
     // the synchronous send ensures that the handler object is
     // available in SessionStore.jsm immediately upon loading
     // content-sessionStore.js.
     sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this});
   },
 
   collectSessionHistory: function (includePrivateData) {
-    let history = SessionHistory.collect(docShell);
-    if ("index" in history) {
-      let tabIndex = history.index - 1;
-      TextAndScrollData.updateFrame(history.entries[tabIndex],
-                                    content,
-                                    docShell.isAppTab,
-                                    {includePrivateData: includePrivateData});
-    }
-    return history;
+    return SessionHistory.collect(docShell);
   },
 
   /**
    * This function is used to make the tab process flush all data that
    * hasn't been sent to the parent process, yet.
    *
    * @param id (int)
    *        A unique id that represents the last message received by the chrome
@@ -303,16 +280,61 @@ let ScrollPositionListener = {
   },
 
   collect: function () {
     return gFrameTree.map(ScrollPosition.collect);
   }
 };
 
 /**
+ * Listens for changes to input elements. Whenever the value of an input
+ * element changes we will re-collect data for the current frame tree and send
+ * a message to the parent process.
+ *
+ * Causes a SessionStore:update message to be sent that contains the form data
+ * for all reachable frames.
+ *
+ * Example:
+ *   {
+ *     formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
+ *     children: [
+ *       null,
+ *       {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
+ *     ]
+ *   }
+ */
+let FormDataListener = {
+  init: function () {
+    addEventListener("input", this, true);
+    addEventListener("change", this, true);
+    gFrameTree.addObserver(this);
+  },
+
+  handleEvent: function (event) {
+    let frame = event.target &&
+                event.target.ownerDocument &&
+                event.target.ownerDocument.defaultView;
+
+    // Don't collect form data for frames created at or after the load event
+    // as SessionStore can't restore form data for those.
+    if (frame && gFrameTree.contains(frame)) {
+      MessageQueue.push("formdata", () => this.collect());
+    }
+  },
+
+  onFrameTreeReset: function () {
+    MessageQueue.push("formdata", () => null);
+  },
+
+  collect: function () {
+    return gFrameTree.map(FormData.collect);
+  }
+};
+
+/**
  * Listens for changes to the page style. Whenever a different page style is
  * selected or author styles are enabled/disabled we send a message with the
  * currently applied style to the chrome process.
  *
  * Causes a SessionStore:update message to be sent that contains the currently
  * selected pageStyle for all reachable frames.
  *
  * Example:
@@ -621,15 +643,16 @@ let MessageQueue = {
     }
 
     this.send();
   }
 };
 
 EventListener.init();
 MessageListener.init();
+FormDataListener.init();
 SyncHandler.init();
 ProgressListener.init();
 PageStyleListener.init();
 SessionStorageListener.init();
 ScrollPositionListener.init();
 DocShellCapabilitiesListener.init();
 PrivacyListener.init();
--- a/browser/components/sessionstore/src/ContentRestore.jsm
+++ b/browser/components/sessionstore/src/ContentRestore.jsm
@@ -8,26 +8,26 @@ this.EXPORTED_SYMBOLS = ["ContentRestore
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+  "resource:///modules/sessionstore/FormData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource:///modules/sessionstore/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
-  "resource:///modules/sessionstore/TextAndScrollData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource:///modules/sessionstore/Utils.jsm");
 
 /**
  * This module implements the content side of session restoration. The chrome
  * side is handled by SessionStore.jsm. The functions in this module are called
  * by content-sessionStore.js based on messages received from SessionStore.jsm
  * (or, in one case, based on a "load" event). Each tab has its own
@@ -86,17 +86,17 @@ function ContentRestoreInternal(chromeGl
 
   // The epoch that was passed into restoreHistory. Removed in restoreDocument.
   this._epoch = 0;
 
   // The tabData for the restore. Set in restoreHistory and removed in
   // restoreTabContent.
   this._tabData = null;
 
-  // Contains {entry, pageStyle, scrollPositions}, where entry is a
+  // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
   // single entry from the tabData.entries array. Set in
   // restoreTabContent and removed in restoreDocument.
   this._restoringDocument = null;
 
   // This listener is used to detect reloads on restoring tabs. Set in
   // restoreHistory and removed in restoreTabContent.
   this._historyListener = null;
 
@@ -202,16 +202,17 @@ ContentRestoreInternal.prototype = {
         // Load userTypedValue and fix up the URL if it's partial/broken.
         webNavigation.loadURI(tabData.userTypedValue,
                               Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
                               null, null, null);
       } else if (tabData.entries.length) {
         // Stash away the data we need for restoreDocument.
         let activeIndex = tabData.index - 1;
         this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
+                                   formdata: tabData.formdata || {},
                                    pageStyle: tabData.pageStyle || {},
                                    scrollPositions: tabData.scroll || {}};
 
         // In order to work around certain issues in session history, we need to
         // force session history to update its internal index and call reload
         // instead of gotoIndex. See bug 597315.
         history.getEntryAtIndex(activeIndex, true);
         history.reloadCurrentEntry();
@@ -272,31 +273,47 @@ ContentRestoreInternal.prototype = {
    * called when the "load" event fires for the restoring tab.
    */
   restoreDocument: function () {
     this._epoch = 0;
 
     if (!this._restoringDocument) {
       return;
     }
-    let {entry, pageStyle, scrollPositions} = this._restoringDocument;
+    let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
     this._restoringDocument = null;
 
     let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
     let frameList = this.getFramesToRestore(window, entry);
 
     // Support the old pageStyle format.
     if (typeof(pageStyle) === "string") {
       PageStyle.restore(this.docShell, frameList, pageStyle);
     } else {
       PageStyle.restoreTree(this.docShell, pageStyle);
     }
 
+    FormData.restoreTree(window, formdata);
     ScrollPosition.restoreTree(window, scrollPositions);
-    TextAndScrollData.restore(frameList);
+
+    // We need to support the old form and scroll data for a while at least.
+    for (let [frame, data] of frameList) {
+      if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) {
+        let formdata = data.formdata || {};
+        formdata.url = data.url;
+
+        if (data.hasOwnProperty("innerHTML")) {
+          formdata.innerHTML = data.innerHTML;
+        }
+
+        FormData.restore(frame, formdata);
+      }
+
+      ScrollPosition.restore(frame, data.scroll || "");
+    }
   },
 
   /**
    * Cancel an ongoing restore. This function can be called any time between
    * restoreHistory and restoreDocument.
    *
    * This function is called externally (if a restore is canceled) and
    * internally (when the loads for a restore have finished). In the latter
deleted file mode 100644
--- a/browser/components/sessionstore/src/DocumentUtils.jsm
+++ /dev/null
@@ -1,233 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [ "DocumentUtils" ];
-
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
-
-this.DocumentUtils = {
-  /**
-   * Obtain form data for a DOMDocument instance.
-   *
-   * The returned object has 2 keys, "id" and "xpath". Each key holds an object
-   * which further defines form data.
-   *
-   * The "id" object maps element IDs to values. The "xpath" object maps the
-   * XPath of an element to its value.
-   *
-   * @param  aDocument
-   *         DOMDocument instance to obtain form data for.
-   * @return object
-   *         Form data encoded in an object.
-   */
-  getFormData: function DocumentUtils_getFormData(aDocument) {
-    let formNodes = aDocument.evaluate(
-      XPathGenerator.restorableFormNodes,
-      aDocument,
-      XPathGenerator.resolveNS,
-      Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
-    );
-
-    let node;
-    let ret = {id: {}, xpath: {}};
-
-    // Limit the number of XPath expressions for performance reasons. See
-    // bug 477564.
-    const MAX_TRAVERSED_XPATHS = 100;
-    let generatedCount = 0;
-
-    while (node = formNodes.iterateNext()) {
-      let nId = node.id;
-      let hasDefaultValue = true;
-      let value;
-
-      // Only generate a limited number of XPath expressions for perf reasons
-      // (cf. bug 477564)
-      if (!nId && generatedCount > MAX_TRAVERSED_XPATHS) {
-        continue;
-      }
-
-      if (node instanceof Ci.nsIDOMHTMLInputElement ||
-          node instanceof Ci.nsIDOMHTMLTextAreaElement ||
-          node instanceof Ci.nsIDOMXULTextBoxElement) {
-        switch (node.type) {
-          case "checkbox":
-          case "radio":
-            value = node.checked;
-            hasDefaultValue = value == node.defaultChecked;
-            break;
-          case "file":
-            value = { type: "file", fileList: node.mozGetFileNameArray() };
-            hasDefaultValue = !value.fileList.length;
-            break;
-          default: // text, textarea
-            value = node.value;
-            hasDefaultValue = value == node.defaultValue;
-            break;
-        }
-      } else if (!node.multiple) {
-        // <select>s without the multiple attribute are hard to determine the
-        // default value, so assume we don't have the default.
-        hasDefaultValue = false;
-        value = { selectedIndex: node.selectedIndex, value: node.value };
-      } else {
-        // <select>s with the multiple attribute are easier to determine the
-        // default value since each <option> has a defaultSelected
-        let options = Array.map(node.options, function(aOpt, aIx) {
-          let oSelected = aOpt.selected;
-          hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected);
-          return oSelected ? aOpt.value : -1;
-        });
-        value = options.filter(function(aIx) aIx !== -1);
-      }
-
-      // In order to reduce XPath generation (which is slow), we only save data
-      // for form fields that have been changed. (cf. bug 537289)
-      if (!hasDefaultValue) {
-        if (nId) {
-          ret.id[nId] = value;
-        } else {
-          generatedCount++;
-          ret.xpath[XPathGenerator.generate(node)] = value;
-        }
-      }
-    }
-
-    return ret;
-  },
-
-  /**
-   * Merges form data on a document from previously obtained data.
-   *
-   * This is the inverse of getFormData(). The data argument is the same object
-   * type which is returned by getFormData(): an object containing the keys
-   * "id" and "xpath" which are each objects mapping element identifiers to
-   * form values.
-   *
-   * Where the document has existing form data for an element, the value
-   * will be replaced. Where the document has a form element but no matching
-   * data in the passed object, the element is untouched.
-   *
-   * @param  aDocument
-   *         DOMDocument instance to which to restore form data.
-   * @param  aData
-   *         Object defining form data.
-   */
-  mergeFormData: function DocumentUtils_mergeFormData(aDocument, aData) {
-    if ("xpath" in aData) {
-      for each (let [xpath, value] in Iterator(aData.xpath)) {
-        let node = XPathGenerator.resolve(aDocument, xpath);
-
-        if (node) {
-          this.restoreFormValue(node, value, aDocument);
-        }
-      }
-    }
-
-    if ("id" in aData) {
-      for each (let [id, value] in Iterator(aData.id)) {
-        let node = aDocument.getElementById(id);
-
-        if (node) {
-          this.restoreFormValue(node, value, aDocument);
-        }
-      }
-    }
-  },
-
-  /**
-   * Low-level function to restore a form value to a DOMNode.
-   *
-   * If you want a higher-level interface, see mergeFormData().
-   *
-   * When the value is changed, the function will fire the appropriate DOM
-   * events.
-   *
-   * @param  aNode
-   *         DOMNode to set form value on.
-   * @param  aValue
-   *         Value to set form element to.
-   * @param  aDocument [optional]
-   *         DOMDocument node belongs to. If not defined, node.ownerDocument
-   *         is used.
-   */
-  restoreFormValue: function DocumentUtils_restoreFormValue(aNode, aValue, aDocument) {
-    aDocument = aDocument || aNode.ownerDocument;
-
-    let eventType;
-
-    if (typeof aValue == "string" && aNode.type != "file") {
-      // Don't dispatch an input event if there is no change.
-      if (aNode.value == aValue) {
-        return;
-      }
-
-      aNode.value = aValue;
-      eventType = "input";
-    } else if (typeof aValue == "boolean") {
-      // Don't dispatch a change event for no change.
-      if (aNode.checked == aValue) {
-        return;
-      }
-
-      aNode.checked = aValue;
-      eventType = "change";
-    } else if (typeof aValue == "number") {
-      // handle select backwards compatibility, example { "#id" : index }
-      // We saved the value blindly since selects take more work to determine
-      // default values. So now we should check to avoid unnecessary events.
-      if (aNode.selectedIndex == aValue) {
-        return;
-      }
-
-      if (aValue < aNode.options.length) {
-        aNode.selectedIndex = aValue;
-        eventType = "change";
-      }
-    } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
-      // handle select new format
-
-      // Don't dispatch a change event for no change
-      if (aNode.options[aNode.selectedIndex].value == aValue.value) {
-        return;
-      }
-
-      // find first option with matching aValue if possible
-      for (let i = 0; i < aNode.options.length; i++) {
-        if (aNode.options[i].value == aValue.value) {
-          aNode.selectedIndex = i;
-          eventType = "change";
-          break;
-        }
-      }
-    } else if (aValue && aValue.fileList && aValue.type == "file" &&
-      aNode.type == "file") {
-      aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
-      eventType = "input";
-    } else if (aValue && typeof aValue.indexOf == "function" && aNode.options) {
-      Array.forEach(aNode.options, function(opt, index) {
-        // don't worry about malformed options with same values
-        opt.selected = aValue.indexOf(opt.value) > -1;
-
-        // Only fire the event here if this wasn't selected by default
-        if (!opt.defaultSelected) {
-          eventType = "change";
-        }
-      });
-    }
-
-    // Fire events for this node if applicable
-    if (eventType) {
-      let event = aDocument.createEvent("UIEvents");
-      event.initUIEvent(eventType, true, true, aDocument.defaultView, 0);
-      aNode.dispatchEvent(event);
-    }
-  }
-};
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/src/FormData.jsm
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FormData"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
+
+/**
+ * Returns whether the given URL very likely has input
+ * fields that contain serialized session store data.
+ */
+function isRestorationPage(url) {
+  return url == "about:sessionrestore" || url == "about:welcomeback";
+}
+
+/**
+ * Returns whether the given form |data| object contains nested restoration
+ * data for a page like about:sessionrestore or about:welcomeback.
+ */
+function hasRestorationData(data) {
+  if (isRestorationPage(data.url) && data.id) {
+    return typeof(data.id.sessionData) == "object";
+  }
+
+  return false;
+}
+
+/**
+ * Returns the given document's current URI and strips
+ * off the URI's anchor part, if any.
+ */
+function getDocumentURI(doc) {
+  return doc.documentURI.replace(/#.*$/, "");
+}
+
+/**
+ * The public API exported by this module that allows to collect
+ * and restore form data for a document and its subframes.
+ */
+this.FormData = Object.freeze({
+  collect: function (frame) {
+    return FormDataInternal.collect(frame);
+  },
+
+  restore: function (frame, data) {
+    FormDataInternal.restore(frame, data);
+  },
+
+  restoreTree: function (root, data) {
+    FormDataInternal.restoreTree(root, data);
+  }
+});
+
+/**
+ * This module's internal API.
+ */
+let FormDataInternal = {
+  /**
+   * Collect form data for a given |frame| *not* including any subframes.
+   *
+   * The returned object may have an "id", "xpath", or "innerHTML" key or a
+   * combination of those three. Form data stored under "id" is for input
+   * fields with id attributes. Data stored under "xpath" is used for input
+   * fields that don't have a unique id and need to be queried using XPath.
+   * The "innerHTML" key is used for editable documents (designMode=on).
+   *
+   * Example:
+   *   {
+   *     id: {input1: "value1", input3: "value3"},
+   *     xpath: {
+   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
+   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
+   *     }
+   *   }
+   *
+   * @param  doc
+   *         DOMDocument instance to obtain form data for.
+   * @return object
+   *         Form data encoded in an object.
+   */
+  collect: function ({document: doc}) {
+    let formNodes = doc.evaluate(
+      XPathGenerator.restorableFormNodes,
+      doc,
+      XPathGenerator.resolveNS,
+      Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
+    );
+
+    let node;
+    let ret = {};
+
+    // Limit the number of XPath expressions for performance reasons. See
+    // bug 477564.
+    const MAX_TRAVERSED_XPATHS = 100;
+    let generatedCount = 0;
+
+    while (node = formNodes.iterateNext()) {
+      let hasDefaultValue = true;
+      let value;
+
+      // Only generate a limited number of XPath expressions for perf reasons
+      // (cf. bug 477564)
+      if (!node.id && generatedCount > MAX_TRAVERSED_XPATHS) {
+        continue;
+      }
+
+      if (node instanceof Ci.nsIDOMHTMLInputElement ||
+          node instanceof Ci.nsIDOMHTMLTextAreaElement ||
+          node instanceof Ci.nsIDOMXULTextBoxElement) {
+        switch (node.type) {
+          case "checkbox":
+          case "radio":
+            value = node.checked;
+            hasDefaultValue = value == node.defaultChecked;
+            break;
+          case "file":
+            value = { type: "file", fileList: node.mozGetFileNameArray() };
+            hasDefaultValue = !value.fileList.length;
+            break;
+          default: // text, textarea
+            value = node.value;
+            hasDefaultValue = value == node.defaultValue;
+            break;
+        }
+      } else if (!node.multiple) {
+        // <select>s without the multiple attribute are hard to determine the
+        // default value, so assume we don't have the default.
+        hasDefaultValue = false;
+        value = { selectedIndex: node.selectedIndex, value: node.value };
+      } else {
+        // <select>s with the multiple attribute are easier to determine the
+        // default value since each <option> has a defaultSelected property
+        let options = Array.map(node.options, opt => {
+          hasDefaultValue = hasDefaultValue && (opt.selected == opt.defaultSelected);
+          return opt.selected ? opt.value : -1;
+        });
+        value = options.filter(ix => ix > -1);
+      }
+
+      // In order to reduce XPath generation (which is slow), we only save data
+      // for form fields that have been changed. (cf. bug 537289)
+      if (hasDefaultValue) {
+        continue;
+      }
+
+      if (node.id) {
+        ret.id = ret.id || {};
+        ret.id[node.id] = value;
+      } else {
+        generatedCount++;
+        ret.xpath = ret.xpath || {};
+        ret.xpath[XPathGenerator.generate(node)] = value;
+      }
+    }
+
+    // designMode is undefined e.g. for XUL documents (as about:config)
+    if ((doc.designMode || "") == "on" && doc.body) {
+      ret.innerHTML = doc.body.innerHTML;
+    }
+
+    // Return |null| if no form data has been found.
+    if (Object.keys(ret).length === 0) {
+      return null;
+    }
+
+    // Store the frame's current URL with its form data so that we can compare
+    // it when restoring data to not inject form data into the wrong document.
+    ret.url = getDocumentURI(doc);
+
+    // We want to avoid saving data for about:sessionrestore as a string.
+    // Since it's stored in the form as stringified JSON, stringifying further
+    // causes an explosion of escape characters. cf. bug 467409
+    if (isRestorationPage(ret.url)) {
+      ret.id.sessionData = JSON.parse(ret.id.sessionData);
+    }
+
+    return ret;
+  },
+
+  /**
+   * Restores form |data| for the given frame. The data is expected to be in
+   * the same format that FormData.collect() returns.
+   *
+   * @param frame (DOMWindow)
+   *        The frame to restore form data to.
+   * @param data (object)
+   *        An object holding form data.
+   */
+  restore: function ({document: doc}, data) {
+    // Don't restore any data for the given frame if the URL
+    // stored in the form data doesn't match its current URL.
+    if (!data.url || data.url != getDocumentURI(doc)) {
+      return;
+    }
+
+    // For about:{sessionrestore,welcomeback} we saved the field as JSON to
+    // avoid nested instances causing humongous sessionstore.js files.
+    // cf. bug 467409
+    if (hasRestorationData(data)) {
+      data.id.sessionData = JSON.stringify(data.id.sessionData);
+    }
+
+    if ("id" in data) {
+      let retrieveNode = id => doc.getElementById(id);
+      this.restoreManyInputValues(data.id, retrieveNode);
+    }
+
+    if ("xpath" in data) {
+      let retrieveNode = xpath => XPathGenerator.resolve(doc, xpath);
+      this.restoreManyInputValues(data.xpath, retrieveNode);
+    }
+
+    if ("innerHTML" in data) {
+      // We know that the URL matches data.url right now, but the user
+      // may navigate away before the setTimeout handler runs. We do
+      // a simple comparison against savedURL to check for that.
+      let savedURL = doc.documentURI;
+
+      setTimeout(() => {
+        if (doc.body && doc.designMode == "on" && doc.documentURI == savedURL) {
+          doc.body.innerHTML = data.innerHTML;
+        }
+      });
+    }
+  },
+
+  /**
+   * Iterates the given form data, retrieving nodes for all the keys and
+   * restores their appropriate values.
+   *
+   * @param data (object)
+   *        A subset of the form data as collected by FormData.collect(). This
+   *        is either data stored under "id" or under "xpath".
+   * @param retrieve (function)
+   *        The function used to retrieve the input field belonging to a key
+   *        in the given |data| object.
+   */
+  restoreManyInputValues: function (data, retrieve) {
+    for (let key of Object.keys(data)) {
+      let input = retrieve(key);
+      if (input) {
+        this.restoreSingleInputValue(input, data[key]);
+      }
+    }
+  },
+
+  /**
+   * Restores a given form value to a given DOMNode and takes care of firing
+   * the appropriate DOM event should the input's value change.
+   *
+   * @param  aNode
+   *         DOMNode to set form value on.
+   * @param  aValue
+   *         Value to set form element to.
+   */
+  restoreSingleInputValue: function (aNode, aValue) {
+    let eventType;
+
+    if (typeof aValue == "string" && aNode.type != "file") {
+      // Don't dispatch an input event if there is no change.
+      if (aNode.value == aValue) {
+        return;
+      }
+
+      aNode.value = aValue;
+      eventType = "input";
+    } else if (typeof aValue == "boolean") {
+      // Don't dispatch a change event for no change.
+      if (aNode.checked == aValue) {
+        return;
+      }
+
+      aNode.checked = aValue;
+      eventType = "change";
+    } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
+      // Don't dispatch a change event for no change
+      if (aNode.options[aNode.selectedIndex].value == aValue.value) {
+        return;
+      }
+
+      // find first option with matching aValue if possible
+      for (let i = 0; i < aNode.options.length; i++) {
+        if (aNode.options[i].value == aValue.value) {
+          aNode.selectedIndex = i;
+          eventType = "change";
+          break;
+        }
+      }
+    } else if (aValue && aValue.fileList && aValue.type == "file" &&
+      aNode.type == "file") {
+      aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
+      eventType = "input";
+    } else if (Array.isArray(aValue) && aNode.options) {
+      Array.forEach(aNode.options, function(opt, index) {
+        // don't worry about malformed options with same values
+        opt.selected = aValue.indexOf(opt.value) > -1;
+
+        // Only fire the event here if this wasn't selected by default
+        if (!opt.defaultSelected) {
+          eventType = "change";
+        }
+      });
+    }
+
+    // Fire events for this node if applicable
+    if (eventType) {
+      let doc = aNode.ownerDocument;
+      let event = doc.createEvent("UIEvents");
+      event.initUIEvent(eventType, true, true, doc.defaultView, 0);
+      aNode.dispatchEvent(event);
+    }
+  },
+
+  /**
+   * Restores form data for the current frame hierarchy starting at |root|
+   * using the given form |data|.
+   *
+   * If the given |root| frame's hierarchy doesn't match that of the given
+   * |data| object we will silently discard data for unreachable frames. For
+   * security reasons we will never restore form data to the wrong frames as
+   * we bail out silently if the stored URL doesn't match the frame's current
+   * URL.
+   *
+   * @param root (DOMWindow)
+   * @param data (object)
+   *        {
+   *          formdata: {id: {input1: "value1"}},
+   *          children: [
+   *            {formdata: {id: {input2: "value2"}}},
+   *            null,
+   *            {formdata: {xpath: { ... }}, children: [ ... ]}
+   *          ]
+   *        }
+   */
+  restoreTree: function (root, data) {
+    // Don't restore any data for the root frame and its subframes if there
+    // is a URL stored in the form data and it doesn't match its current URL.
+    if (data.url && data.url != getDocumentURI(root.document)) {
+      return;
+    }
+
+    if (data.url) {
+      this.restore(root, data);
+    }
+
+    if (!data.hasOwnProperty("children")) {
+      return;
+    }
+
+    let frames = root.frames;
+    for (let index of Object.keys(data.children)) {
+      if (index < frames.length) {
+        this.restoreTree(frames[index], data.children[index]);
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/src/PrivacyLevelFilter.jsm
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PrivacyLevelFilter"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
+  "resource:///modules/sessionstore/PrivacyLevel.jsm");
+
+/**
+ * Returns whether the current privacy level allows saving data for the given
+ * |url|.
+ *
+ * @param url The URL we want to save data for.
+ * @param isPinned Whether the given |url| is contained in a pinned tab.
+ * @return bool
+ */
+function checkPrivacyLevel(url, isPinned) {
+  let isHttps = url.startsWith("https:");
+  return PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned});
+}
+
+/**
+ * A module that provides methods to filter various kinds of data collected
+ * from a tab by the current privacy level as set by the user.
+ */
+this.PrivacyLevelFilter = Object.freeze({
+  /**
+   * Filters the given (serialized) session storage |data| according to the
+   * current privacy level and returns a new object containing only data that
+   * we're allowed to store.
+   *
+   * @param data The session storage data as collected from a tab.
+   * @param isPinned Whether the tab we collected from is pinned.
+   * @return object
+   */
+  filterSessionStorageData: function (data, isPinned) {
+    let retval = {};
+
+    for (let host of Object.keys(data)) {
+      if (checkPrivacyLevel(host, isPinned)) {
+        retval[host] = data[host];
+      }
+    }
+
+    return Object.keys(retval).length ? retval : null;
+  },
+
+  /**
+   * Filters the given (serialized) form |data| according to the current
+   * privacy level and returns a new object containing only data that we're
+   * allowed to store.
+   *
+   * @param data The form data as collected from a tab.
+   * @param isPinned Whether the tab we collected from is pinned.
+   * @return object
+   */
+  filterFormData: function (data, isPinned) {
+    // If the given form data object has an associated URL that we are not
+    // allowed to store data for, bail out. We explicitly discard data for any
+    // children as well even if storing data for those frames would be allowed.
+    if (data.url && !checkPrivacyLevel(data.url, isPinned)) {
+      return;
+    }
+
+    let retval = {};
+
+    for (let key of Object.keys(data)) {
+      if (key === "children") {
+        let recurse = child => this.filterFormData(child, isPinned);
+        let children = data.children.map(recurse).filter(child => child);
+
+        if (children.length) {
+          retval.children = children;
+        }
+      // Only copy keys other than "children" if we have a valid URL in
+      // data.url and we thus passed the privacy level check.
+      } else if (data.url) {
+        retval[key] = data[key];
+      }
+    }
+
+    return Object.keys(retval).length ? retval : null;
+  }
+});
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -47,21 +47,16 @@ const WINDOW_ATTRIBUTES = ["width", "hei
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
 const WINDOW_HIDEABLE_FEATURES = [
   "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
 ];
 
 const MESSAGES = [
-  // The content script tells us that its form data (or that of one of its
-  // subframes) might have changed. This can be the contents or values of
-  // standard form fields or of ContentEditables.
-  "SessionStore:input",
-
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
   "SessionStore:pageshow",
 
   // The content script tells us that a new page just started loading in a
   // browser.
   "SessionStore:loadStart",
@@ -609,19 +604,16 @@ let SessionStoreInternal = {
   receiveMessage: function ssi_receiveMessage(aMessage) {
     var browser = aMessage.target;
     var win = browser.ownerDocument.defaultView;
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
-      case "SessionStore:input":
-        this.onTabInput(win, browser);
-        break;
       case "SessionStore:loadStart":
         TabStateCache.delete(browser);
         break;
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
       case "SessionStore:update":
         this.recordTelemetry(aMessage.data.telemetry);
@@ -1440,28 +1432,16 @@ let SessionStoreInternal = {
     delete aBrowser.__SS_data;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
 
   /**
-   * Called when a browser sends the "input" notification
-   * @param aWindow
-   *        Window reference
-   * @param aBrowser
-   *        Browser reference
-   */
-  onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
-    TabStateCache.delete(aBrowser);
-    this.saveStateDelayed(aWindow);
-  },
-
-  /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
   onTabSelect: function ssi_onTabSelect(aWindow) {
     if (this._loadState == STATE_RUNNING) {
       this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
 
@@ -2734,16 +2714,17 @@ let SessionStoreInternal = {
       browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
       browser.setAttribute("pending", "true");
       tab.setAttribute("pending", "true");
 
       // Update the persistent tab state cache with |tabData| information.
       TabStateCache.updatePersistent(browser, {
         scroll: tabData.scroll || null,
         storage: tabData.storage || null,
+        formdata: tabData.formdata || null,
         disallow: tabData.disallow || null,
         pageStyle: tabData.pageStyle || null
       });
 
       browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
                                               {tabData: tabData, epoch: epoch});
 
       // wall-paper fix for bug 439675: make sure that the URL to be loaded
--- a/browser/components/sessionstore/src/TabState.jsm
+++ b/browser/components/sessionstore/src/TabState.jsm
@@ -11,18 +11,18 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
   "resource:///modules/sessionstore/Messenger.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
-  "resource:///modules/sessionstore/PrivacyLevel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevelFilter",
+  "resource:///modules/sessionstore/PrivacyLevelFilter.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
   "resource:///modules/sessionstore/TabStateCache.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
   "resource:///modules/sessionstore/TabAttributes.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource:///modules/sessionstore/Utils.jsm");
 
 /**
@@ -161,18 +161,17 @@ let TabStateInternal = {
     if (!this._tabNeedsExtraCollection(tab)) {
       let tabData = this._collectBaseTabData(tab);
       return Promise.resolve(tabData);
     }
 
     let browser = tab.linkedBrowser;
 
     let promise = Task.spawn(function task() {
-      // Collect session history data asynchronously. Also collects
-      // text and scroll data.
+      // Collect session history data asynchronously.
       let history = yield Messenger.send(tab, "SessionStore:collectSessionHistory");
 
       // The tab could have been closed while waiting for a response.
       if (!tab.linkedBrowser) {
         return;
       }
 
       // Collect basic tab data, without session history and storage.
@@ -349,43 +348,37 @@ let TabStateInternal = {
    *        The tab belonging to the given |tabData| object.
    * @param tabData (object)
    *        The tab data belonging to the given |tab|.
    * @param options (object)
    *        {includePrivateData: true} to always include private data
    */
   _copyFromPersistentCache: function (tab, tabData, options = {}) {
     let data = TabStateCache.getPersistent(tab.linkedBrowser);
-
-    // Nothing to do without any cached data.
     if (!data) {
       return;
     }
 
+    // The caller may explicitly request to omit privacy checks.
     let includePrivateData = options && options.includePrivateData;
 
     for (let key of Object.keys(data)) {
-      if (key != "storage" || includePrivateData) {
-        tabData[key] = data[key];
-      } else {
-        let storage = {};
-        let isPinned = tab.pinned;
+      let value = data[key];
 
-        // If we're not allowed to include private data, let's filter out hosts
-        // based on the given tab's pinned state and the privacy level.
-        for (let host of Object.keys(data.storage)) {
-          let isHttps = host.startsWith("https:");
-          if (PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-            storage[host] = data.storage[host];
-          }
+      // Filter sensitive data according to the current privacy level.
+      if (!includePrivateData) {
+        if (key === "storage") {
+          value = PrivacyLevelFilter.filterSessionStorageData(value, tab.pinned);
+        } else if (key === "formdata") {
+          value = PrivacyLevelFilter.filterFormData(value, tab.pinned);
         }
+      }
 
-        if (Object.keys(storage).length) {
-          tabData.storage = storage;
-        }
+      if (value) {
+        tabData[key] = value;
       }
     }
   },
 
   /*
    * Returns true if the xul:tab element is newly added (i.e., if it's
    * showing about:blank with no history).
    */
deleted file mode 100644
--- a/browser/components/sessionstore/src/TextAndScrollData.jsm
+++ /dev/null
@@ -1,145 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
-* License, v. 2.0. If a copy of the MPL was not distributed with this file,
-* You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["TextAndScrollData"];
-
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
-  "resource:///modules/sessionstore/DocumentUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
-  "resource:///modules/sessionstore/PrivacyLevel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
-  "resource:///modules/sessionstore/ScrollPosition.jsm");
-
-/**
- * The external API exported by this module.
- */
-this.TextAndScrollData = Object.freeze({
-  updateFrame: function (entry, content, isPinned, options) {
-    return TextAndScrollDataInternal.updateFrame(entry, content, isPinned, options);
-  },
-
-  restore: function (frameList) {
-    TextAndScrollDataInternal.restore(frameList);
-  },
-});
-
-let TextAndScrollDataInternal = {
-  /**
-   * Go through all subframes and store all form data, the current
-   * scroll positions and innerHTML content of WYSIWYG editors.
-   *
-   * @param entry
-   *        the object into which to store the collected data
-   * @param content
-   *        frame reference
-   * @param isPinned
-   *        the tab is pinned and should be treated differently for privacy
-   * @param includePrivateData
-   *        {includePrivateData:true} include privacy sensitive data (use with care)
-   */
-  updateFrame: function (entry, content, isPinned, options = null) {
-    let includePrivateData = options && options.includePrivateData;
-
-    for (let i = 0; i < content.frames.length; i++) {
-      if (entry.children && entry.children[i]) {
-        this.updateFrame(entry.children[i], content.frames[i], includePrivateData, isPinned);
-      }
-    }
-
-    let href = (content.parent || content).document.location.href;
-    let isHttps = Services.io.newURI(href, null, null).schemeIs("https");
-    let topURL = content.top.document.location.href;
-    let isAboutSR = this.isAboutSessionRestore(topURL);
-    if (includePrivateData || isAboutSR ||
-        PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-      let formData = DocumentUtils.getFormData(content.document);
-
-      // We want to avoid saving data for about:sessionrestore as a string.
-      // Since it's stored in the form as stringified JSON, stringifying further
-      // causes an explosion of escape characters. cf. bug 467409
-      if (formData && isAboutSR) {
-        formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
-      }
-
-      if (Object.keys(formData.id).length ||
-          Object.keys(formData.xpath).length) {
-        entry.formdata = formData;
-      }
-
-      // designMode is undefined e.g. for XUL documents (as about:config)
-      if ((content.document.designMode || "") == "on" && content.document.body) {
-        entry.innerHTML = content.document.body.innerHTML;
-      }
-    }
-  },
-
-  isAboutSessionRestore: function (url) {
-    return url == "about:sessionrestore" || url == "about:welcomeback";
-  },
-
-  restore: function (frameList) {
-    for (let [frame, data] of frameList) {
-      this.restoreFrame(frame, data);
-    }
-  },
-
-  restoreFrame: function (content, data) {
-    if (data.formdata) {
-      let formdata = data.formdata;
-
-      // handle backwards compatibility
-      // this is a migration from pre-firefox 15. cf. bug 742051
-      if (!("xpath" in formdata || "id" in formdata)) {
-        formdata = { xpath: {}, id: {} };
-
-        for each (let [key, value] in Iterator(data.formdata)) {
-          if (key.charAt(0) == "#") {
-            formdata.id[key.slice(1)] = value;
-          } else {
-            formdata.xpath[key] = value;
-          }
-        }
-      }
-
-      // for about:sessionrestore we saved the field as JSON to avoid
-      // nested instances causing humongous sessionstore.js files.
-      // cf. bug 467409
-      if (this.isAboutSessionRestore(data.url) &&
-          "sessionData" in formdata.id &&
-          typeof formdata.id["sessionData"] == "object") {
-        formdata.id["sessionData"] = JSON.stringify(formdata.id["sessionData"]);
-      }
-
-      // update the formdata
-      data.formdata = formdata;
-      // merge the formdata
-      DocumentUtils.mergeFormData(content.document, formdata);
-    }
-
-    if (data.innerHTML) {
-      // We know that the URL matches data.url right now, but the user
-      // may navigate away before the setTimeout handler runs. We do
-      // a simple comparison against savedURL to check for that.
-      let savedURL = content.document.location.href;
-
-      setTimeout(function() {
-        if (content.document.designMode == "on" &&
-            content.document.location.href == savedURL &&
-            content.document.body) {
-          content.document.body.innerHTML = data.innerHTML;
-        }
-      }, 0);
-    }
-
-    ScrollPosition.restore(content, data.scroll || "");
-  },
-};
--- a/browser/components/sessionstore/src/moz.build
+++ b/browser/components/sessionstore/src/moz.build
@@ -10,33 +10,33 @@ EXTRA_COMPONENTS += [
     'nsSessionStore.manifest',
 ]
 
 JS_MODULES_PATH = 'modules/sessionstore'
 
 EXTRA_JS_MODULES = [
     'ContentRestore.jsm',
     'DocShellCapabilities.jsm',
-    'DocumentUtils.jsm',
+    'FormData.jsm',
     'FrameTree.jsm',
     'Messenger.jsm',
     'PageStyle.jsm',
     'PrivacyLevel.jsm',
+    'PrivacyLevelFilter.jsm',
     'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
     'ScrollPosition.jsm',
     'SessionCookies.jsm',
     'SessionFile.jsm',
     'SessionHistory.jsm',
     'SessionMigration.jsm',
     'SessionStorage.jsm',
     'SessionWorker.js',
     'TabAttributes.jsm',
     'TabState.jsm',
     'TabStateCache.jsm',
-    'TextAndScrollData.jsm',
     'Utils.jsm',
     'XPathGenerator.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'SessionSaver.jsm',
     'SessionStore.jsm',
 ]