Backed out 2 changesets (bug 1507286, bug 1497146) for causing multiple crashes in nsFocusManager::GetRedirectedFocus a=backout
authorCiure Andrei <aciure@mozilla.com>
Sat, 26 Jan 2019 15:09:03 +0200
changeset 455536 b08b9f22ad06e55aa83e9c85c74db82c50552094
parent 455535 cd565d21faa62b32b62228e927400220035aadd0
child 455537 3c9dd9f66747e807faadaaf4d37661728ae78836
child 455546 bb2895bfd1bc3d83c309e904dbe74e0c60c3fac9
child 455554 a711808372f9d2c51f9b5b046ea62aa7a8d24c26
push id111479
push useraciure@mozilla.com
push dateSat, 26 Jan 2019 13:53:11 +0000
treeherdermozilla-inbound@3c9dd9f66747 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1507286, 1497146
milestone66.0a1
backs out0509a9edc58aa8f5a2a51e9684f93c913d6d55ab
21ad3aeb636f3e28853927b4a7b36262320d6f58
first release with
nightly linux32
b08b9f22ad06 / 66.0a1 / 20190126132917 / files
nightly linux64
b08b9f22ad06 / 66.0a1 / 20190126132917 / files
nightly mac
b08b9f22ad06 / 66.0a1 / 20190126132917 / files
nightly win32
b08b9f22ad06 / 66.0a1 / 20190126132917 / files
nightly win64
b08b9f22ad06 / 66.0a1 / 20190126132917 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 2 changesets (bug 1507286, bug 1497146) for causing multiple crashes in nsFocusManager::GetRedirectedFocus a=backout Backed out changeset 0509a9edc58a (bug 1507286) Backed out changeset 21ad3aeb636f (bug 1497146)
browser/app/profile/firefox.js
browser/components/sessionstore/ContentRestore.jsm
browser/components/sessionstore/ContentSessionStore.jsm
browser/components/sessionstore/SessionStorage.jsm
browser/components/sessionstore/moz.build
dom/base/Document.h
dom/chrome-webidl/SessionStoreUtils.webidl
dom/xslt/xpath/XPathEvaluator.h
dom/xslt/xpath/moz.build
dom/xslt/xpath/txIXPathContext.h
mobile/android/chrome/geckoview/GeckoViewContentChild.js
mobile/android/components/SessionStore.js
modules/libpref/init/StaticPrefList.h
toolkit/components/sessionstore/SessionStoreUtils.cpp
toolkit/components/sessionstore/SessionStoreUtils.h
toolkit/modules/moz.build
toolkit/modules/sessionstore/FormData.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -853,16 +853,18 @@ pref("browser.sessionstore.upgradeBackup
 // End-users should not run sessionstore in debug mode
 pref("browser.sessionstore.debug", false);
 // Causes SessionStore to ignore non-final update messages from
 // browser tabs that were not caused by a flush from the parent.
 // This is a testing flag and should not be used by end-users.
 pref("browser.sessionstore.debug.no_auto_updates", false);
 // Forget closed windows/tabs after two weeks
 pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000);
+// Maximum number of bytes of DOMSessionStorage data we collect per origin.
+pref("browser.sessionstore.dom_storage_limit", 2048);
 // Amount of failed SessionFile writes until we restart the worker.
 pref("browser.sessionstore.max_write_failures", 5);
 
 // Whether to warn the user when quitting, even though their tabs will be restored.
 pref("browser.sessionstore.warnOnQuit", false);
 
 // allow META refresh by default
 pref("accessibility.blockautorefresh", false);
--- a/browser/components/sessionstore/ContentRestore.jsm
+++ b/browser/components/sessionstore/ContentRestore.jsm
@@ -3,18 +3,22 @@
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ContentRestore"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 
+ChromeUtils.defineModuleGetter(this, "FormData",
+  "resource://gre/modules/FormData.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionHistory",
   "resource://gre/modules/sessionstore/SessionHistory.jsm");
+ChromeUtils.defineModuleGetter(this, "SessionStorage",
+  "resource:///modules/sessionstore/SessionStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "Utils",
   "resource://gre/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
@@ -132,17 +136,17 @@ ContentRestoreInternal.prototype = {
     this._historyListener = listener;
 
     // Make sure to reset the capabilities and attributes in case this tab gets
     // reused.
     SessionStoreUtils.restoreDocShellCapabilities(this.docShell, tabData.disallow);
 
 
     if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) {
-      SessionStoreUtils.restoreSessionStorage(this.docShell, tabData.storage);
+      SessionStorage.restore(this.docShell, tabData.storage);
       delete tabData.storage;
     }
 
     // Add a progress listener to correctly handle browser.loadURI()
     // calls from foreign code.
     this._progressListener = new ProgressListener(this.docShell, {
       onStartRequest: () => {
         // Some code called browser.loadURI() on a pending tab. It's safe to
@@ -294,17 +298,17 @@ ContentRestoreInternal.prototype = {
 
     let window = this.docShell.domWindow;
 
     // Restore form data.
     Utils.restoreFrameTreeData(window, formdata, (frame, data) => {
       // restore() will return false, and thus abort restoration for the
       // current |frame| and its descendants, if |data.url| is given but
       // doesn't match the loaded document's URL.
-      return SessionStoreUtils.restoreFormData(frame.document, data);
+      return FormData.restore(frame, data);
     });
 
     // Restore scroll data.
     Utils.restoreFrameTreeData(window, scrollPositions, (frame, data) => {
       if (data.scroll) {
         SessionStoreUtils.restoreScrollPosition(frame, data);
       }
     });
--- a/browser/components/sessionstore/ContentSessionStore.jsm
+++ b/browser/components/sessionstore/ContentSessionStore.jsm
@@ -13,16 +13,18 @@ ChromeUtils.import("resource://gre/modul
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
 ChromeUtils.defineModuleGetter(this, "ContentRestore",
   "resource:///modules/sessionstore/ContentRestore.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionHistory",
   "resource://gre/modules/sessionstore/SessionHistory.jsm");
+ChromeUtils.defineModuleGetter(this, "SessionStorage",
+  "resource:///modules/sessionstore/SessionStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "Utils",
   "resource://gre/modules/sessionstore/Utils.jsm");
 
 // A bound to the size of data to store for DOM Storage.
 const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
 
 // This pref controls whether or not we send updates to the parent on a timeout
 // or not, and should only be used for tests or debugging.
@@ -526,20 +528,17 @@ class SessionStorageListener extends Han
     }
 
     let {content} = this.mm;
 
     // We need the entire session storage, let's reset the pending individual change
     // messages.
     this.resetChanges();
 
-    this.messageQueue.push("storage", () => {
-      let data = SessionStoreUtils.collectSessionStorage(content);
-      return Object.keys(data).length ? data : null;
-    });
+    this.messageQueue.push("storage", () => SessionStorage.collect(content));
   }
 
   onPageLoadCompleted() {
     this.collect();
   }
 
   onPageLoadStarted() {
     this.resetEventListener();
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/SessionStorage.jsm
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["SessionStorage"];
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// A bound to the size of data to store for DOM Storage.
+const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
+
+// Returns the principal for a given |frame| contained in a given |docShell|.
+function getPrincipalForFrame(docShell, frame) {
+  let ssm = Services.scriptSecurityManager;
+  let uri = frame.document.documentURIObject;
+  return ssm.getDocShellCodebasePrincipal(uri, docShell);
+}
+
+var SessionStorage = Object.freeze({
+  /**
+   * Updates all sessionStorage "super cookies"
+   * @param content
+   *        A tab's global, i.e. the root frame we want to collect for.
+   * @return Returns a nested object that will have hosts as keys and per-origin
+   *         session storage data as strings. For example:
+   *         {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
+   */
+  collect(content) {
+    return SessionStorageInternal.collect(content);
+  },
+
+  /**
+   * Restores all sessionStorage "super cookies".
+   * @param aDocShell
+   *        A tab's docshell (containing the sessionStorage)
+   * @param aStorageData
+   *        A nested object with storage data to be restored that has hosts as
+   *        keys and per-origin session storage data as strings. For example:
+   *        {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
+   */
+  restore(aDocShell, aStorageData) {
+    SessionStorageInternal.restore(aDocShell, aStorageData);
+  },
+});
+
+/**
+ * Calls the given callback |cb|, passing |frame| and each of its descendants.
+ */
+function forEachNonDynamicChildFrame(frame, cb) {
+  // Call for current frame.
+  cb(frame);
+
+  // Call the callback recursively for each descendant.
+  SessionStoreUtils.forEachNonDynamicChildFrame(frame, subframe => {
+    return forEachNonDynamicChildFrame(subframe, cb);
+  });
+}
+
+var SessionStorageInternal = {
+  /**
+   * Reads all session storage data from the given docShell.
+   * @param content
+   *        A tab's global, i.e. the root frame we want to collect for.
+   * @return Returns a nested object that will have hosts as keys and per-origin
+   *         session storage data as strings. For example:
+   *         {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
+   */
+  collect(content) {
+    let data = {};
+    let visitedOrigins = new Set();
+    let docShell = content.docShell;
+
+    forEachNonDynamicChildFrame(content, frame => {
+      let principal = getPrincipalForFrame(docShell, frame);
+      if (!principal) {
+        return;
+      }
+
+      // Get the origin of the current history entry
+      // and use that as a key for the per-principal storage data.
+      let origin;
+      try {
+        // The origin getter may throw for about:blank iframes as of bug 1340710,
+        // but we should ignore them anyway.
+        origin = principal.origin;
+      } catch (e) {
+        return;
+      }
+      if (visitedOrigins.has(origin)) {
+        // Don't read a host twice.
+        return;
+      }
+
+      // Mark the current origin as visited.
+      visitedOrigins.add(origin);
+
+      let originData = this._readEntry(principal, docShell);
+      if (Object.keys(originData).length) {
+        data[origin] = originData;
+      }
+    });
+
+    return Object.keys(data).length ? data : null;
+  },
+
+  /**
+   * Writes session storage data to the given tab.
+   * @param aDocShell
+   *        A tab's docshell (containing the sessionStorage)
+   * @param aStorageData
+   *        A nested object with storage data to be restored that has hosts as
+   *        keys and per-origin session storage data as strings. For example:
+   *        {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
+   */
+  restore(aDocShell, aStorageData) {
+    for (let origin of Object.keys(aStorageData)) {
+      let data = aStorageData[origin];
+
+      let principal;
+
+      try {
+        // NOTE: In capture() we record the full origin for the URI which the
+        // sessionStorage is being captured for. As of bug 1235657 this code
+        // stopped parsing any origins which have originattributes correctly, as
+        // it decided to use the origin attributes from the docshell, and try to
+        // interpret the origin as a URI. Since bug 1353844 this code now correctly
+        // parses the full origin, and then discards the origin attributes, to
+        // make the behavior line up with the original intentions in bug 1235657
+        // while preserving the ability to read all session storage from
+        // previous versions. In the future, if this behavior is desired, we may
+        // want to use the spec instead of the origin as the key, and avoid
+        // transmitting origin attribute information which we then discard when
+        // restoring.
+        //
+        // If changing this logic, make sure to also change the principal
+        // computation logic in SessionStore::_sendRestoreHistory.
+        let attrs = aDocShell.getOriginAttributes();
+        let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
+        principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs);
+      } catch (e) {
+        console.error(e);
+        continue;
+      }
+
+      let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+
+      // There is no need to pass documentURI, it's only used to fill
+      // documentURI property of domstorage event, which in this case has no
+      // consumer. Prevention of events in case of missing documentURI will be
+      // solved in a followup bug to bug 600307.
+      // Null window because the current window doesn't match the principal yet
+      // and loads about:blank.
+      let storage = storageManager.createStorage(null, principal, "", aDocShell.usePrivateBrowsing);
+
+      for (let key of Object.keys(data)) {
+        try {
+          storage.setItem(key, data[key]);
+        } catch (e) {
+          // throws e.g. for URIs that can't have sessionStorage
+          console.error(e);
+        }
+      }
+    }
+  },
+
+  /**
+   * Reads an entry in the session storage data contained in a tab's history.
+   * @param aURI
+   *        That history entry uri
+   * @param aDocShell
+   *        A tab's docshell (containing the sessionStorage)
+   */
+  _readEntry(aPrincipal, aDocShell) {
+    let hostData = {};
+    let storage;
+
+    let window = aDocShell.domWindow;
+
+    try {
+      let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+      storage = storageManager.getStorage(window, aPrincipal);
+      storage.length; // XXX: Bug 1232955 - storage.length can throw, catch that failure
+    } catch (e) {
+      // sessionStorage might throw if it's turned off, see bug 458954
+      storage = null;
+    }
+
+    if (!storage || !storage.length) {
+      return hostData;
+    }
+
+    // If the DOMSessionStorage contains too much data, ignore it.
+    let usage = window.windowUtils.getStorageUsage(storage);
+    if (usage > Services.prefs.getIntPref(DOM_STORAGE_LIMIT_PREF)) {
+      return hostData;
+    }
+
+    for (let i = 0; i < storage.length; i++) {
+      try {
+        let key = storage.key(i);
+        hostData[key] = storage.getItem(key);
+      } catch (e) {
+        // This currently throws for secured items (cf. bug 442048).
+      }
+    }
+
+    return hostData;
+  },
+};
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -15,16 +15,17 @@ EXTRA_JS_MODULES.sessionstore = [
     'GlobalState.jsm',
     'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
     'RunState.jsm',
     'SessionCookies.jsm',
     'SessionFile.jsm',
     'SessionMigration.jsm',
     'SessionSaver.jsm',
     'SessionStartup.jsm',
+    'SessionStorage.jsm',
     'SessionStore.jsm',
     'SessionWorker.js',
     'SessionWorker.jsm',
     'StartupPerformance.jsm',
     'TabAttributes.jsm',
     'TabState.jsm',
     'TabStateCache.jsm',
     'TabStateFlusher.jsm',
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -3570,18 +3570,16 @@ class Document : public nsINode,
   // When the doc is blocked permanantly, we would dispatch event to notify
   // front-end side to show blocking icon.
   void MaybeNotifyAutoplayBlocked();
 
   // Sets flags for media autoplay telemetry.
   void SetDocTreeHadAudibleMedia();
   void SetDocTreeHadPlayRevoked();
 
-  mozilla::dom::XPathEvaluator* XPathEvaluator();
-
  protected:
   void DoUpdateSVGUseElementShadowTrees();
 
   already_AddRefed<nsIPrincipal> MaybeDowngradePrincipal(
       nsIPrincipal* aPrincipal);
 
   void EnsureOnloadBlocker();
 
@@ -3701,16 +3699,18 @@ class Document : public nsINode,
   friend class mozAutoSubtreeModified;
 
   virtual Element* GetNameSpaceElement() override { return GetRootElement(); }
 
   void SetContentTypeInternal(const nsACString& aType);
 
   nsCString GetContentTypeInternal() const { return mContentType; }
 
+  mozilla::dom::XPathEvaluator* XPathEvaluator();
+
   // Update our frame request callback scheduling state, if needed.  This will
   // schedule or unschedule them, if necessary, and update
   // mFrameRequestCallbacksScheduled.  aOldShell should only be passed when
   // mPresShell is becoming null; in that case it will be used to get hold of
   // the relevant refresh driver.
   void UpdateFrameRequestCallbackSchedulingState(
       nsIPresShell* aOldShell = nullptr);
 
--- a/dom/chrome-webidl/SessionStoreUtils.webidl
+++ b/dom/chrome-webidl/SessionStoreUtils.webidl
@@ -106,38 +106,16 @@ namespace SessionStoreUtils {
    *   }
    *
    * @param  doc
    *         DOMDocument instance to obtain form data for.
    * @return object
    *         Form data encoded in an object.
    */
   CollectedFormData collectFormData(Document document);
-  boolean restoreFormData(Document document, optional CollectedFormData data);
-
-  /**
-   * Updates all sessionStorage "super cookies"
-   * @param content
-   *        A tab's global, i.e. the root frame we want to collect for.
-   * @return Returns a nested object that will have hosts as keys and per-origin
-   *         session storage data as strings. For example:
-   *         {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
-   */
-  record<DOMString, record<DOMString, DOMString>> collectSessionStorage(WindowProxy window);
-
-  /**
-   * Restores all sessionStorage "super cookies".
-   * @param aDocShell
-   *        A tab's docshell (containing the sessionStorage)
-   * @param aStorageData
-   *        A nested object with storage data to be restored that has hosts as
-   *        keys and per-origin session storage data as strings. For example:
-   *        {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
-   */
-   void restoreSessionStorage(nsIDocShell docShell, record<DOMString, record<DOMString, DOMString>> data);
 };
 
 dictionary SSScrollPositionDict {
   ByteString scroll;
 };
 
 dictionary CollectedFileListValue
 {
@@ -147,17 +125,17 @@ dictionary CollectedFileListValue
 
 dictionary CollectedNonMultipleSelectValue
 {
   required long selectedIndex;
   required DOMString value;
 };
 
 // object contains either a CollectedFileListValue or a CollectedNonMultipleSelectValue or Sequence<DOMString>
-typedef (DOMString or boolean or object) CollectedFormDataValue;
+typedef (DOMString or boolean or long or object) CollectedFormDataValue;
 
 dictionary CollectedFormData
 {
   record<DOMString, CollectedFormDataValue> id;
   record<DOMString, CollectedFormDataValue> xpath;
   DOMString innerHTML;
   ByteString url;
 };
--- a/dom/xslt/xpath/XPathEvaluator.h
+++ b/dom/xslt/xpath/XPathEvaluator.h
@@ -43,26 +43,27 @@ class XPathEvaluator final : public NonR
   }
   static XPathEvaluator* Constructor(const GlobalObject& aGlobal,
                                      ErrorResult& rv);
   XPathExpression* CreateExpression(const nsAString& aExpression,
                                     XPathNSResolver* aResolver,
                                     ErrorResult& rv);
   XPathExpression* CreateExpression(const nsAString& aExpression,
                                     nsINode* aResolver, ErrorResult& aRv);
-  XPathExpression* CreateExpression(const nsAString& aExpression,
-                                    txIParseContext* aContext,
-                                    Document* aDocument, ErrorResult& aRv);
   nsINode* CreateNSResolver(nsINode& aNodeResolver) { return &aNodeResolver; }
   already_AddRefed<XPathResult> Evaluate(
       JSContext* aCx, const nsAString& aExpression, nsINode& aContextNode,
       XPathNSResolver* aResolver, uint16_t aType, JS::Handle<JSObject*> aResult,
       ErrorResult& rv);
 
  private:
+  XPathExpression* CreateExpression(const nsAString& aExpression,
+                                    txIParseContext* aContext,
+                                    Document* aDocument, ErrorResult& aRv);
+
   nsWeakPtr mDocument;
   RefPtr<txResultRecycler> mRecycler;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif /* mozilla_dom_XPathEvaluator_h */
--- a/dom/xslt/xpath/moz.build
+++ b/dom/xslt/xpath/moz.build
@@ -1,16 +1,15 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXPORTS.mozilla.dom += [
-    'txIXPathContext.h',
     'XPathEvaluator.h',
     'XPathExpression.h',
     'XPathResult.h',
 ]
 
 UNIFIED_SOURCES += [
     'txBooleanExpr.cpp',
     'txBooleanResult.cpp',
--- a/dom/xslt/xpath/txIXPathContext.h
+++ b/dom/xslt/xpath/txIXPathContext.h
@@ -1,19 +1,17 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef __TX_I_XPATH_CONTEXT
 #define __TX_I_XPATH_CONTEXT
 
-#include "nscore.h"
-#include "nsISupportsImpl.h"
-#include "nsStringFwd.h"
+#include "txCore.h"
 
 class FunctionCall;
 class nsAtom;
 class txAExprResult;
 class txResultRecycler;
 class txXPathNode;
 
 /*
--- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 ChromeUtils.import("resource://gre/modules/GeckoViewChildModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  FormData: "resource://gre/modules/FormData.jsm",
   FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
   GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm",
   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
   Services: "resource://gre/modules/Services.jsm",
   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
 });
 
 class GeckoViewContentChild extends GeckoViewChildModule {
@@ -197,17 +198,17 @@ class GeckoViewContentChild extends Geck
 
           addEventListener("load", _ => {
             const formdata = this._savedState.formdata;
             if (formdata) {
               this.Utils.restoreFrameTreeData(content, formdata, (frame, data) => {
                 // restore() will return false, and thus abort restoration for the
                 // current |frame| and its descendants, if |data.url| is given but
                 // doesn't match the loaded document's URL.
-                return SessionStoreUtils.restoreFormData(frame.document, data);
+                return FormData.restore(frame, data);
               });
             }
           }, {capture: true, mozSystemGroup: true, once: true});
 
           addEventListener("pageshow", _ => {
             const scrolldata = this._savedState.scrolldata;
             if (scrolldata) {
               this.Utils.restoreFrameTreeData(content, scrolldata, (frame, data) => {
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -4,16 +4,17 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
+  FormData: "resource://gre/modules/FormData.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
   SharedPreferences: "resource://gre/modules/SharedPreferences.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
 });
 
@@ -1389,17 +1390,17 @@ SessionStore.prototype = {
   */
   _restoreTextData(aFormData, aBrowser) {
     if (aFormData) {
       log("_restoreTextData()");
       Utils.restoreFrameTreeData(aBrowser.contentWindow, aFormData, (frame, data) => {
         // restore() will return false, and thus abort restoration for the
         // current |frame| and its descendants, if |data.url| is given but
         // doesn't match the loaded document's URL.
-        return SessionStoreUtils.restoreFormData(frame.document, data);
+        return FormData.restore(frame, data);
       });
     }
   },
 
   /**
    * Restores the zoom level of the window. This needs to be called before
    * first paint/load (whichever comes first) to take any effect.
    */
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -1797,26 +1797,16 @@ PREF("network.predictor.cleaned-up", boo
 // A testing flag.
 VARCACHE_PREF(
   "network.predictor.doing-tests",
    network_predictor_doing_tests,
   bool, false
 )
 
 //---------------------------------------------------------------------------
-// ContentSessionStore prefs
-//---------------------------------------------------------------------------
-// Maximum number of bytes of DOMSessionStorage data we collect per origin.
-VARCACHE_PREF(
-  "browser.sessionstore.dom_storage_limit",
-  browser_sessionstore_dom_storage_limit,
-  uint32_t, 2048
-)
-
-//---------------------------------------------------------------------------
 // Preferences prefs
 //---------------------------------------------------------------------------
 
 PREF("preferences.allow.omt-write", bool, true)
 
 //---------------------------------------------------------------------------
 // Privacy prefs
 //---------------------------------------------------------------------------
--- a/toolkit/components/sessionstore/SessionStoreUtils.cpp
+++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp
@@ -1,23 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "js/JSON.h"
-#include "jsapi.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/HTMLSelectElement.h"
 #include "mozilla/dom/HTMLTextAreaElement.h"
 #include "mozilla/dom/SessionStoreUtils.h"
-#include "mozilla/dom/txIXPathContext.h"
 #include "mozilla/dom/WindowProxyHolder.h"
-#include "mozilla/dom/XPathResult.h"
-#include "mozilla/dom/XPathEvaluator.h"
-#include "mozilla/dom/XPathExpression.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentList.h"
 #include "nsContentUtils.h"
 #include "nsFocusManager.h"
 #include "nsGlobalWindowOuter.h"
 #include "nsIDocShell.h"
 #include "nsIFormControl.h"
 #include "nsIScrollableFrame.h"
@@ -322,21 +317,21 @@ static bool IsValidCCNumber(nsAString& a
   return numLength >= 12 && total % 10 == 0;
 }
 
 // Limit the number of XPath expressions for performance reasons. See bug
 // 477564.
 static const uint16_t kMaxTraversedXPaths = 100;
 
 // A helper function to append a element into mId or mXpath of CollectedFormData
-static Record<nsString, OwningStringOrBooleanOrObject>::EntryType*
+static Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType*
 AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId,
                            uint16_t& aGeneratedCount,
                            CollectedFormData& aRetVal) {
-  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry;
+  Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry;
   if (!aId.IsEmpty()) {
     if (!aRetVal.mId.WasPassed()) {
       aRetVal.mId.Construct();
     }
     auto& recordEntries = aRetVal.mId.Value().Entries();
     entry = recordEntries.AppendElement();
     entry->mKey = aId;
   } else {
@@ -386,17 +381,17 @@ static void CollectFromTextAreaElement(D
     nsAutoString value;
     textArea->GetValue(value);
     // In order to reduce XPath generation (which is slow), we only save data
     // for form fields that have been changed. (cf. bug 537289)
     if (textArea->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                               eCaseMatters)) {
       continue;
     }
-    Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+    Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
         AppendEntryToCollectedData(textArea, id, aGeneratedCount, aRetVal);
     entry->mValue.SetAsString() = value;
   }
 }
 
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
   @param aGeneratedCount: the current number of XPath expressions in the
@@ -441,17 +436,17 @@ static void CollectFromInputElement(JSCo
     }
     nsAutoString value;
     if (input->ControlType() == NS_FORM_INPUT_CHECKBOX ||
         input->ControlType() == NS_FORM_INPUT_RADIO) {
       bool checked = input->Checked();
       if (checked == input->DefaultChecked()) {
         continue;
       }
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal);
       entry->mValue.SetAsBoolean() = checked;
     } else if (input->ControlType() == NS_FORM_INPUT_FILE) {
       IgnoredErrorResult rv;
       nsTArray<nsString> result;
       input->MozGetFileNameArray(result, rv);
       if (rv.Failed() || result.Length() == 0) {
         continue;
@@ -460,17 +455,17 @@ static void CollectFromInputElement(JSCo
       val.mType = NS_LITERAL_STRING("file");
       val.mFileList.SwapElements(result);
 
       JS::Rooted<JS::Value> jsval(aCx);
       if (!ToJSValue(aCx, val, &jsval)) {
         JS_ClearPendingException(aCx);
         continue;
       }
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal);
       entry->mValue.SetAsObject() = &jsval.toObject();
     } else {
       input->GetValue(value, CallerType::System);
       // In order to reduce XPath generation (which is slow), we only save data
       // for form fields that have been changed. (cf. bug 537289)
       // Also, don't want to collect credit card number.
       if (value.IsEmpty() || IsValidCCNumber(value) ||
@@ -486,28 +481,28 @@ static void CollectFromInputElement(JSCo
         if (id.EqualsLiteral("sessionData")) {
           nsAutoCString url;
           Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
           if (url.EqualsLiteral("about:sessionrestore") ||
               url.EqualsLiteral("about:welcomeback")) {
             JS::Rooted<JS::Value> jsval(aCx);
             if (JS_ParseJSON(aCx, value.get(), value.Length(), &jsval) &&
                 jsval.isObject()) {
-              Record<nsString, OwningStringOrBooleanOrObject>::EntryType*
+              Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType*
                   entry = AppendEntryToCollectedData(input, id, aGeneratedCount,
                                                      aRetVal);
               entry->mValue.SetAsObject() = &jsval.toObject();
             } else {
               JS_ClearPendingException(aCx);
             }
             continue;
           }
         }
       }
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal);
       entry->mValue.SetAsString() = value;
     }
   }
 }
 
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
@@ -548,30 +543,30 @@ static void CollectFromSelectElement(JSC
       val.mSelectedIndex = select->SelectedIndex();
       val.mValue = selectVal.AsAString();
 
       JS::Rooted<JS::Value> jsval(aCx);
       if (!ToJSValue(aCx, val, &jsval)) {
         JS_ClearPendingException(aCx);
         continue;
       }
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(select, id, aGeneratedCount, aRetVal);
       entry->mValue.SetAsObject() = &jsval.toObject();
     } else {
       // <select>s with the multiple attribute are easier to determine the
       // default value since each <option> has a defaultSelected property
       HTMLOptionsCollection* options = select->GetOptions();
       if (!options) {
         continue;
       }
       bool hasDefaultValue = true;
       nsTArray<nsString> selectslist;
-      uint32_t numOptions = options->Length();
-      for (uint32_t idx = 0; idx < numOptions; idx++) {
+      int numOptions = options->Length();
+      for (int idx = 0; idx < numOptions; idx++) {
         HTMLOptionElement* option = options->ItemAsOption(idx);
         bool selected = option->Selected();
         if (!selected) {
           continue;
         }
         option->GetValue(*selectslist.AppendElement());
         hasDefaultValue =
             hasDefaultValue && (selected == option->DefaultSelected());
@@ -581,17 +576,17 @@ static void CollectFromSelectElement(JSC
       if (hasDefaultValue) {
         continue;
       }
       JS::Rooted<JS::Value> jsval(aCx);
       if (!ToJSValue(aCx, selectslist, &jsval)) {
         JS_ClearPendingException(aCx);
         continue;
       }
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(select, id, aGeneratedCount, aRetVal);
       entry->mValue.SetAsObject() = &jsval.toObject();
     }
   }
 }
 
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
@@ -630,17 +625,17 @@ static void CollectFromXULTextbox(Docume
       }
       input->GetValue(value, CallerType::System);
       if (value.IsEmpty() ||
           input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                              eCaseMatters)) {
         continue;
       }
       uint16_t generatedCount = 0;
-      Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
           AppendEntryToCollectedData(input, id, generatedCount, aRetVal);
       entry->mValue.SetAsString() = value;
       return;
     }
   }
 }
 
 /* static */ void SessionStoreUtils::CollectFormData(
@@ -668,479 +663,8 @@ static void CollectFromXULTextbox(Docume
   }
   // 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.
   nsIURI* uri = aDocument.GetDocumentURI();
   if (uri) {
     uri->GetSpecIgnoringRef(aRetVal.mUrl.Construct());
   }
 }
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsString(Element* aElement, const nsAString& aValue) {
-  IgnoredErrorResult rv;
-  HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNodeOrNull(aElement);
-  if (textArea) {
-    textArea->SetValue(aValue, rv);
-    if (!rv.Failed()) {
-      nsContentUtils::DispatchInputEvent(aElement);
-    }
-    return;
-  }
-  HTMLInputElement* input = HTMLInputElement::FromNodeOrNull(aElement);
-  if (input) {
-    input->SetValue(aValue, CallerType::NonSystem, rv);
-    if (!rv.Failed()) {
-      nsContentUtils::DispatchInputEvent(aElement);
-      return;
-    }
-  }
-  input = HTMLInputElement::FromNodeOrNull(nsFocusManager::GetRedirectedFocus(aElement));
-  if (input) {
-    input->SetValue(aValue, CallerType::NonSystem, rv);
-    if (!rv.Failed()) {
-      nsContentUtils::DispatchInputEvent(aElement);
-    }
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsBool(Element* aElement, bool aValue) {
-  HTMLInputElement* input = HTMLInputElement::FromNodeOrNull(aElement);
-  if (input) {
-    bool checked = input->Checked();
-    if (aValue != checked) {
-      input->SetChecked(aValue);
-      nsContentUtils::DispatchInputEvent(aElement);
-    }
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsFiles(HTMLInputElement* aElement,
-                              const CollectedFileListValue& aValue) {
-  nsTArray<nsString> fileList;
-  IgnoredErrorResult rv;
-  aElement->MozSetFileNameArray(aValue.mFileList, rv);
-  if (rv.Failed()) {
-    return;
-  }
-  nsContentUtils::DispatchInputEvent(aElement);
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsSelect(HTMLSelectElement* aElement,
-                               const CollectedNonMultipleSelectValue& aValue) {
-  HTMLOptionsCollection* options = aElement->GetOptions();
-  if (!options) {
-    return;
-  }
-  int32_t selectIdx = options->SelectedIndex();
-  if (selectIdx >= 0) {
-    nsAutoString selectOptionVal;
-    options->ItemAsOption(selectIdx)->GetValue(selectOptionVal);
-    if (aValue.mValue.Equals(selectOptionVal)) {
-      return;
-    }
-  }
-  uint32_t numOptions = options->Length();
-  for (uint32_t idx = 0; idx < numOptions; idx++) {
-    HTMLOptionElement* option = options->ItemAsOption(idx);
-    nsAutoString optionValue;
-    option->GetValue(optionValue);
-    if (aValue.mValue.Equals(optionValue)) {
-      aElement->SetSelectedIndex(idx);
-      nsContentUtils::DispatchInputEvent(aElement);
-    }
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsMultiSelect(HTMLSelectElement* aElement,
-                                    const nsTArray<nsString>& aValueArray) {
-  bool fireEvent = false;
-  HTMLOptionsCollection* options = aElement->GetOptions();
-  if (!options) {
-    return;
-  }
-  uint32_t numOptions = options->Length();
-  for (uint32_t idx = 0; idx < numOptions; idx++) {
-    HTMLOptionElement* option = options->ItemAsOption(idx);
-    nsAutoString optionValue;
-    option->GetValue(optionValue);
-    for (uint32_t i = 0, l = aValueArray.Length(); i < l; ++i) {
-      if (optionValue.Equals(aValueArray[i])) {
-        option->SetSelected(true);
-        if (!option->DefaultSelected()) {
-          fireEvent = true;
-        }
-      }
-    }
-  }
-  if (fireEvent) {
-    nsContentUtils::DispatchInputEvent(aElement);
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetElementAsObject(JSContext* aCx, Element* aElement,
-                               JS::Handle<JS::Value> aObject) {
-  RefPtr<HTMLInputElement> input = HTMLInputElement::FromNodeOrNull(aElement);
-  if (input) {
-    if (input->ControlType() == NS_FORM_INPUT_FILE) {
-      CollectedFileListValue value;
-      if (value.Init(aCx, aObject)) {
-        SetElementAsFiles(input, value);
-      } else {
-        JS_ClearPendingException(aCx);
-      }
-    }
-    return;
-  }
-  RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNodeOrNull(aElement);
-  if (select) {
-    // For Single Select Element
-    if (!select->Multiple()) {
-      CollectedNonMultipleSelectValue value;
-      if (value.Init(aCx, aObject)) {
-        SetElementAsSelect(select, value);
-      } else {
-        JS_ClearPendingException(aCx);
-      }
-      return;
-    }
-
-    // For Multiple Selects Element
-    bool isArray = false;
-    JS_IsArrayObject(aCx, aObject, &isArray);
-    if (!isArray) {
-      return;
-    }
-    JS::Rooted<JSObject*> arrayObj(aCx, &aObject.toObject());
-    uint32_t arrayLength = 0;
-    if (!JS_GetArrayLength(aCx, arrayObj, &arrayLength)) {
-      JS_ClearPendingException(aCx);
-      return;
-    }
-    nsTArray<nsString> array(arrayLength);
-    for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) {
-      JS::Rooted<JS::Value> element(aCx);
-      if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) {
-        JS_ClearPendingException(aCx);
-        return;
-      }
-      if (!element.isString()) {
-        return;
-      }
-      nsAutoJSString value;
-      if (!value.init(aCx, element)) {
-        JS_ClearPendingException(aCx);
-        return;
-      }
-      array.AppendElement(value);
-    }
-    SetElementAsMultiSelect(select, array);
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetRestoreData(JSContext* aCx, Element* aElement,
-                           JS::MutableHandle<JS::Value> aObject) {
-  nsAutoString data;
-  if (nsContentUtils::StringifyJSON(aCx, aObject, data)) {
-    SetElementAsString(aElement, data);
-  } else {
-    JS_ClearPendingException(aCx);
-  }
-}
-
-MOZ_CAN_RUN_SCRIPT
-static void SetInnerHTML(Document& aDocument, const CollectedFormData& aData) {
-  RefPtr<Element> bodyElement = aDocument.GetBody();
-  if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
-    IgnoredErrorResult rv;
-    bodyElement->SetInnerHTML(aData.mInnerHTML.Value(),
-                              aDocument.NodePrincipal(), rv);
-    if (!rv.Failed()) {
-      nsContentUtils::DispatchInputEvent(bodyElement);
-    }
-  }
-}
-
-class FormDataParseContext : public txIParseContext {
- public:
-  explicit FormDataParseContext(bool aCaseInsensitive)
-      : mIsCaseInsensitive(aCaseInsensitive) {}
-
-  nsresult resolveNamespacePrefix(nsAtom* aPrefix, int32_t& aID) override {
-    if (aPrefix == nsGkAtoms::xul) {
-      aID = kNameSpaceID_XUL;
-    } else {
-      MOZ_ASSERT(nsDependentAtomString(aPrefix).EqualsLiteral("xhtml"));
-      aID = kNameSpaceID_XHTML;
-    }
-    return NS_OK;
-  }
-
-  nsresult resolveFunctionCall(nsAtom* aName, int32_t aID,
-                               FunctionCall** aFunction) override {
-    return NS_ERROR_XPATH_UNKNOWN_FUNCTION;
-  }
-
-  bool caseInsensitiveNameTests() override { return mIsCaseInsensitive; }
-
-  void SetErrorOffset(uint32_t aOffset) override {}
-
- private:
-  bool mIsCaseInsensitive;
-};
-
-static Element* FindNodeByXPath(JSContext* aCx, Document& aDocument,
-                                const nsAString& aExpression) {
-  FormDataParseContext parsingContext(aDocument.IsHTMLDocument());
-  IgnoredErrorResult rv;
-  nsAutoPtr<XPathExpression> expression(
-      aDocument.XPathEvaluator()->CreateExpression(aExpression, &parsingContext,
-                                                   &aDocument, rv));
-  if (rv.Failed()) {
-    return nullptr;
-  }
-  RefPtr<XPathResult> result = expression->Evaluate(
-      aCx, aDocument, XPathResult::FIRST_ORDERED_NODE_TYPE, nullptr, rv);
-  if (rv.Failed()) {
-    return nullptr;
-  }
-  return Element::FromNodeOrNull(result->GetSingleNodeValue(rv));
-}
-
-MOZ_CAN_RUN_SCRIPT_BOUNDARY
-/* static */ bool SessionStoreUtils::RestoreFormData(
-    const GlobalObject& aGlobal, Document& aDocument,
-    const CollectedFormData& aData) {
-  if (!aData.mUrl.WasPassed()) {
-    return true;
-  }
-  // Don't restore any data for the given frame if the URL
-  // stored in the form data doesn't match its current URL.
-  nsAutoCString url;
-  Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
-  if (!aData.mUrl.Value().Equals(url)) {
-    return false;
-  }
-  if (aData.mInnerHTML.WasPassed()) {
-    SetInnerHTML(aDocument, aData);
-  }
-  if (aData.mId.WasPassed()) {
-    for (auto& entry : aData.mId.Value().Entries()) {
-      RefPtr<Element> node = aDocument.GetElementById(entry.mKey);
-      if (entry.mValue.IsString()) {
-        SetElementAsString(node, entry.mValue.GetAsString());
-      } else if (entry.mValue.IsBoolean()) {
-        SetElementAsBool(node, entry.mValue.GetAsBoolean());
-      } else {
-        // For about:{sessionrestore,welcomeback} we saved the field as JSON to
-        // avoid nested instances causing humongous sessionstore.js files.
-        // cf. bug 467409
-        JSContext* cx = aGlobal.Context();
-        if (entry.mKey.EqualsLiteral("sessionData")) {
-          nsAutoCString url;
-          Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
-          if (url.EqualsLiteral("about:sessionrestore") ||
-              url.EqualsLiteral("about:welcomeback")) {
-            JS::Rooted<JS::Value> object(
-                cx, JS::ObjectValue(*entry.mValue.GetAsObject()));
-            SetRestoreData(cx, node, &object);
-            continue;
-          }
-        }
-        JS::Rooted<JS::Value> object(
-            cx, JS::ObjectValue(*entry.mValue.GetAsObject()));
-        SetElementAsObject(cx, node, object);
-      }
-    }
-  }
-  if (aData.mXpath.WasPassed()) {
-    for (auto& entry : aData.mXpath.Value().Entries()) {
-      RefPtr<Element> node = FindNodeByXPath(aGlobal.Context(), aDocument, entry.mKey);
-      if (entry.mValue.IsString()) {
-        SetElementAsString(node, entry.mValue.GetAsString());
-      } else if (entry.mValue.IsBoolean()) {
-        SetElementAsBool(node, entry.mValue.GetAsBoolean());
-      } else {
-        JS::Rooted<JS::Value> object(
-            aGlobal.Context(), JS::ObjectValue(*entry.mValue.GetAsObject()));
-        SetElementAsObject(aGlobal.Context(), node, object);
-      }
-    }
-  }
-  return true;
-}
-
-/* Read entries in the session storage data contained in a tab's history. */
-static void ReadAllEntriesFromStorage(
-    nsPIDOMWindowOuter* aWindow,
-    nsTHashtable<nsCStringHashKey>& aVisitedOrigins,
-    Record<nsString, Record<nsString, nsString>>& aRetVal) {
-  nsCOMPtr<nsIDocShell> docShell = aWindow->GetDocShell();
-  if (!docShell) {
-    return;
-  }
-
-  Document* doc = aWindow->GetDoc();
-  if (!doc) {
-    return;
-  }
-  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
-  if (!principal) {
-    return;
-  }
-
-  nsAutoCString origin;
-  nsresult rv = principal->GetOrigin(origin);
-  if (NS_FAILED(rv) || aVisitedOrigins.Contains(origin)) {
-    // Don't read a host twice.
-    return;
-  }
-
-  /* Completed checking for recursion and is about to read storage*/
-  nsCOMPtr<nsIDOMStorageManager> storageManager = do_QueryInterface(docShell);
-  if (!storageManager) {
-    return;
-  }
-  RefPtr<Storage> storage;
-  storageManager->GetStorage(aWindow->GetCurrentInnerWindow(), principal, false,
-                             getter_AddRefs(storage));
-  if (!storage) {
-    return;
-  }
-  mozilla::IgnoredErrorResult result;
-  uint32_t len = storage->GetLength(*principal, result);
-  if (result.Failed() || len == 0) {
-    return;
-  }
-  int64_t storageUsage = storage->GetOriginQuotaUsage();
-  if (storageUsage > StaticPrefs::browser_sessionstore_dom_storage_limit()) {
-    return;
-  }
-
-  Record<nsString, Record<nsString, nsString>>::EntryType* recordEntry = nullptr;
-  for (uint32_t i = 0; i < len; i++) {
-    Record<nsString, nsString>::EntryType entry;
-    mozilla::IgnoredErrorResult res;
-    storage->Key(i, entry.mKey, *principal, res);
-    if (res.Failed()) {
-      continue;
-    }
-
-    storage->GetItem(entry.mKey, entry.mValue, *principal, res);
-    if (res.Failed()) {
-      continue;
-    }
-
-    if (!recordEntry) {
-      recordEntry = aRetVal.Entries().AppendElement();
-      recordEntry->mKey = NS_ConvertUTF8toUTF16(origin);
-      aVisitedOrigins.PutEntry(origin);
-    }
-    recordEntry->mValue.Entries().AppendElement(std::move(entry));
-  }
-}
-
-/* Collect Collect session storage from current frame and all child frame */
-static void CollectedSessionStorageInternal(
-    JSContext* aCx, BrowsingContext* aBrowsingContext,
-    nsTHashtable<nsCStringHashKey>& aVisitedOrigins,
-    Record<nsString, Record<nsString, nsString>>& aRetVal) {
-  /* Collect session store from current frame */
-  nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow();
-  if (!window) {
-    return;
-  }
-  ReadAllEntriesFromStorage(window, aVisitedOrigins, aRetVal);
-
-  /* Collect session storage from all child frame */
-  nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
-  if (!docShell) {
-    return;
-  }
-  int32_t length;
-  nsresult rv = docShell->GetChildCount(&length);
-  if (NS_FAILED(rv)) {
-    return;
-  }
-  for (int32_t i = 0; i < length; ++i) {
-    nsCOMPtr<nsIDocShellTreeItem> item;
-    docShell->GetChildAt(i, getter_AddRefs(item));
-    if (!item) {
-      return;
-    }
-    nsCOMPtr<nsIDocShell> childDocShell(do_QueryInterface(item));
-    if (!childDocShell) {
-      return;
-    }
-    bool isDynamic = false;
-    rv = childDocShell->GetCreatedDynamically(&isDynamic);
-    if (NS_SUCCEEDED(rv) && isDynamic) {
-      continue;
-    }
-    CollectedSessionStorageInternal(
-        aCx, nsDocShell::Cast(childDocShell)->GetBrowsingContext(),
-        aVisitedOrigins, aRetVal);
-  }
-}
-
-/* static */ void SessionStoreUtils::CollectSessionStorage(
-    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
-    Record<nsString, Record<nsString, nsString>>& aRetVal) {
-  nsTHashtable<nsCStringHashKey> visitedOrigins;
-  CollectedSessionStorageInternal(aGlobal.Context(), aWindow.get(),
-                                  visitedOrigins, aRetVal);
-}
-
-/* static */ void SessionStoreUtils::RestoreSessionStorage(
-    const GlobalObject& aGlobal, nsIDocShell* aDocShell,
-    const Record<nsString, Record<nsString, nsString>>& aData) {
-  for (auto& entry : aData.Entries()) {
-    // NOTE: In capture() we record the full origin for the URI which the
-    // sessionStorage is being captured for. As of bug 1235657 this code
-    // stopped parsing any origins which have originattributes correctly, as
-    // it decided to use the origin attributes from the docshell, and try to
-    // interpret the origin as a URI. Since bug 1353844 this code now correctly
-    // parses the full origin, and then discards the origin attributes, to
-    // make the behavior line up with the original intentions in bug 1235657
-    // while preserving the ability to read all session storage from
-    // previous versions. In the future, if this behavior is desired, we may
-    // want to use the spec instead of the origin as the key, and avoid
-    // transmitting origin attribute information which we then discard when
-    // restoring.
-    //
-    // If changing this logic, make sure to also change the principal
-    // computation logic in SessionStore::_sendRestoreHistory.
-
-    // OriginAttributes are always after a '^' character
-    int32_t pos = entry.mKey.RFindChar('^');
-    nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateCodebasePrincipal(
-        NS_ConvertUTF16toUTF8(Substring(entry.mKey, 0, pos)));
-    nsresult rv;
-    nsCOMPtr<nsIDOMStorageManager> storageManager = do_QueryInterface(aDocShell, &rv);
-    if (NS_FAILED(rv)) {
-      return;
-    }
-    RefPtr<Storage> storage;
-    // There is no need to pass documentURI, it's only used to fill documentURI
-    // property of domstorage event, which in this case has no consumer.
-    // Prevention of events in case of missing documentURI will be solved in a
-    // followup bug to bug 600307.
-    // Null window because the current window doesn't match the principal yet
-    // and loads about:blank.
-    storageManager->CreateStorage(nullptr, principal, EmptyString(), false, getter_AddRefs(storage));
-    if (!storage) {
-      continue;
-    }
-    for (auto& InnerEntry : entry.mValue.Entries()) {
-      IgnoredErrorResult result;
-      storage->SetItem(InnerEntry.mKey, InnerEntry.mValue, *principal, result);
-      if (result.Failed()) {
-        NS_WARNING("storage set item failed!");
-      }
-    }
-  }
-}
--- a/toolkit/components/sessionstore/SessionStoreUtils.h
+++ b/toolkit/components/sessionstore/SessionStoreUtils.h
@@ -49,26 +49,14 @@ class SessionStoreUtils {
                                     SSScrollPositionDict& aRetVal);
 
   static void RestoreScrollPosition(const GlobalObject& aGlobal,
                                     nsGlobalWindowInner& aWindow,
                                     const SSScrollPositionDict& data);
 
   static void CollectFormData(const GlobalObject& aGlobal, Document& aDocument,
                               CollectedFormData& aRetVal);
-
-  MOZ_CAN_RUN_SCRIPT_BOUNDARY
-  static bool RestoreFormData(const GlobalObject& aGlobal, Document& aDocument,
-                              const CollectedFormData& aData);
-
-  static void CollectSessionStorage(
-      const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
-      Record<nsString, Record<nsString, nsString>>& aRetVal);
-
-  static void RestoreSessionStorage(
-      const GlobalObject& aGlobal, nsIDocShell* aDocShell,
-      const Record<nsString, Record<nsString, nsString>>& aData);
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_SessionStoreUtils_h
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -236,16 +236,17 @@ EXTRA_JS_MODULES += [
     'RemoteController.js',
     'RemoteSecurityUI.jsm',
     'RemoteWebProgress.jsm',
     'ResetProfile.jsm',
     'ResponsivenessMonitor.jsm',
     'SelectParentHelper.jsm',
     'ServiceRequest.jsm',
     'Services.jsm',
+    'sessionstore/FormData.jsm',
     'ShortcutUtils.jsm',
     'Sqlite.jsm',
     'Timer.jsm',
     'Troubleshoot.jsm',
     'UpdateUtils.jsm',
     'WebChannel.jsm',
     'WebProgressChild.jsm',
     'ZipUtils.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/sessionstore/FormData.jsm
@@ -0,0 +1,280 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["FormData"];
+
+/**
+ * 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.
+ */
+var FormData = Object.freeze({
+  restore(frame, data) {
+    return FormDataInternal.restore(frame, data);
+  },
+
+  restoreTree(root, data) {
+    FormDataInternal.restoreTree(root, data);
+  },
+});
+
+/**
+ * This module's internal API.
+ */
+var FormDataInternal = {
+  namespaceURIs: {
+    "xhtml": "http://www.w3.org/1999/xhtml",
+    "xul": "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+  },
+
+  /**
+   * Resolves an XPath query generated by node.generateXPath.
+   */
+  resolve(aDocument, aQuery) {
+    let xptype = aDocument.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+    return aDocument.evaluate(aQuery, aDocument, this.resolveNS.bind(this), xptype, null).singleNodeValue;
+  },
+
+  /**
+   * Namespace resolver for the above XPath resolver.
+   */
+  resolveNS(aPrefix) {
+    return this.namespaceURIs[aPrefix] || null;
+  },
+
+  /**
+   * @returns an XPath query to all savable form field nodes
+   */
+  get restorableFormNodesXPath() {
+    let formNodesXPath = "//textarea|//xhtml:textarea|" +
+      "//select|//xhtml:select|" +
+      "//input|//xhtml:input" +
+      // Special case for about:config's search field.
+      "|/xul:window[@id='config']//xul:textbox[@id='textbox']";
+
+    delete this.restorableFormNodesXPath;
+    return (this.restorableFormNodesXPath = formNodesXPath);
+  },
+
+  /**
+   * 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({document: doc}, data) {
+    if (!data.url) {
+      return true;
+    }
+
+    // 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 != getDocumentURI(doc)) {
+      return false;
+    }
+
+    // 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 => this.resolve(doc, xpath);
+      this.restoreManyInputValues(data.xpath, retrieveNode);
+    }
+
+    if ("innerHTML" in data) {
+      if (doc.body && doc.designMode == "on") {
+      // eslint-disable-next-line no-unsanitized/property
+        doc.body.innerHTML = data.innerHTML;
+        this.fireInputEvent(doc.body);
+      }
+    }
+
+    return true;
+  },
+
+  /**
+   * 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(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(aNode, aValue) {
+    let fireEvent = false;
+
+    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;
+      fireEvent = true;
+    } else if (typeof aValue == "boolean") {
+      // Don't dispatch a change event for no change.
+      if (aNode.checked == aValue) {
+        return;
+      }
+
+      aNode.checked = aValue;
+      fireEvent = true;
+    } 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;
+          fireEvent = true;
+          break;
+        }
+      }
+    } else if (aValue && aValue.fileList && aValue.type == "file" &&
+      aNode.type == "file") {
+      try {
+        // FIXME (bug 1122855): This won't work in content processes.
+        aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
+      } catch (e) {
+        Cu.reportError("mozSetFileNameArray: " + e);
+      }
+      fireEvent = true;
+    } 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) {
+          fireEvent = true;
+        }
+      });
+    }
+
+    // Fire events for this node if applicable
+    if (fireEvent) {
+      this.fireInputEvent(aNode);
+    }
+  },
+
+  /**
+   * Dispatches an event of type "input" to the given |node|.
+   *
+   * @param node (DOMNode)
+   */
+  fireInputEvent(node) {
+    // "inputType" value hasn't been decided for session restor:
+    // https://github.com/w3c/input-events/issues/30#issuecomment-438693664
+    let event = node.isInputEventTarget ?
+      new node.ownerGlobal.InputEvent("input", {bubbles: true, inputType: ""}) :
+      new node.ownerGlobal.Event("input", {bubbles: true});
+    node.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(root, data) {
+    // Restore data for the given |root| frame and its descendants. If restore()
+    // returns false this indicates the |data.url| doesn't match the loaded
+    // document URI. We then must ignore this branch for security reasons.
+    if (this.restore(root, data) === false) {
+      return;
+    }
+
+    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]);
+      }
+    }
+  },
+};