Bug 1497146 part 2 - Convert FormData.jsm to C++ [collect() part] r=nika,peterv,mikedeboer
authorAlphan Chen <alchen@mozilla.com>
Thu, 17 Jan 2019 14:56:51 +0000
changeset 511386 b1a1231573cd3922090c48dc9676db8d8371ef58
parent 511385 67fa6cd74c6a138415b7a15959057a6e82c20628
child 511387 cc32943d74cc1623f28d6dedd23beabdfbbbf753
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnika, peterv, mikedeboer
bugs1497146
milestone66.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 1497146 part 2 - Convert FormData.jsm to C++ [collect() part] r=nika,peterv,mikedeboer Differential Revision: https://phabricator.services.mozilla.com/D12112
browser/components/sessionstore/ContentSessionStore.jsm
browser/components/sessionstore/test/browser_formdata.js
dom/chrome-webidl/SessionStoreUtils.webidl
mobile/android/chrome/geckoview/GeckoViewContentChild.js
mobile/android/components/SessionStore.js
toolkit/components/sessionstore/SessionStoreUtils.cpp
toolkit/components/sessionstore/SessionStoreUtils.h
toolkit/modules/sessionstore/FormData.jsm
--- a/browser/components/sessionstore/ContentSessionStore.jsm
+++ b/browser/components/sessionstore/ContentSessionStore.jsm
@@ -9,19 +9,16 @@ var EXPORTED_SYMBOLS = ["ContentSessionS
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
 ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
 ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
-ChromeUtils.defineModuleGetter(this, "FormData",
-  "resource://gre/modules/FormData.jsm");
-
 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");
@@ -379,17 +376,17 @@ class FormDataListener extends Handler {
     this.messageQueue.push("formdata", () => this.collect());
   }
 
   onPageLoadStarted() {
     this.messageQueue.push("formdata", () => null);
   }
 
   collect() {
-    return mapFrameTree(this.mm, FormData.collect);
+    return mapFrameTree(this.mm, SessionStoreUtils.collectFormData);
   }
 }
 
 /**
  * Listens for changes to docShell capabilities. Whenever a new load is started
  * we need to re-check the list of capabilities and send message when it has
  * changed.
  *
--- a/browser/components/sessionstore/test/browser_formdata.js
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -98,18 +98,18 @@ add_task(async function test_url_check()
  */
 add_task(async function test_nested() {
   const URL = "data:text/html;charset=utf-8," +
               "<iframe src='data:text/html;charset=utf-8," +
               "<input autofocus=true>'/>";
 
   const FORM_DATA = {
     children: [{
+      url: "data:text/html;charset=utf-8,<input autofocus=true>",
       xpath: {"/xhtml:html/xhtml:body/xhtml:input": "m"},
-      url: "data:text/html;charset=utf-8,<input autofocus=true>",
     }],
   };
 
   // Create a tab with an iframe containing an input field.
   let tab = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, URL);
   let browser = tab.linkedBrowser;
   await promiseBrowserLoaded(browser);
 
--- a/dom/chrome-webidl/SessionStoreUtils.webidl
+++ b/dom/chrome-webidl/SessionStoreUtils.webidl
@@ -81,13 +81,61 @@ namespace SessionStoreUtils {
 
   /**
    * Restores scroll position data for any given |frame| in the frame hierarchy.
    *
    * @param frame (DOMWindow)
    * @param value (object, see collectScrollPosition())
    */
   void restoreScrollPosition(Window frame, optional SSScrollPositionDict data);
+
+  /**
+   * 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.
+   */
+  CollectedFormData collectFormData(Document document);
 };
 
 dictionary SSScrollPositionDict {
   ByteString scroll;
 };
+
+dictionary CollectedFileListValue
+{
+  required DOMString type;
+  required sequence<DOMString> fileList;
+};
+
+dictionary CollectedNonMultipleSelectValue
+{
+  required long selectedIndex;
+  required DOMString value;
+};
+
+// object contains either a CollectedFileListValue or a CollectedNonMultipleSelectValue or Sequence<DOMString>
+typedef (DOMString or boolean or long or object) CollectedFormDataValue;
+
+dictionary CollectedFormData
+{
+  record<DOMString, CollectedFormDataValue> id;
+  record<DOMString, CollectedFormDataValue> xpath;
+  DOMString innerHTML;
+  ByteString url;
+};
--- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js
@@ -78,17 +78,18 @@ class GeckoViewContentChild extends Geck
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("MozDOMFullscreen:Request", this);
     removeEventListener("contextmenu", this, { capture: true });
   }
 
   collectSessionState() {
     let history = SessionHistory.collect(docShell);
     let [formdata, scrolldata] = this.Utils.mapFrameTree(
-        content, FormData.collect, SessionStoreUtils.collectScrollPosition);
+        content, SessionStoreUtils.collectFormData,
+        SessionStoreUtils.collectScrollPosition);
 
     // Save the current document resolution.
     let zoom = 1;
     let domWindowUtils = content.windowUtils;
     zoom = domWindowUtils.getResolution();
     scrolldata = scrolldata || {};
     scrolldata.zoom = {};
     scrolldata.zoom.resolution = zoom;
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -889,17 +889,17 @@ SessionStore.prototype = {
     let data = aBrowser.__SS_data;
     if (!data || data.entries.length == 0) {
       sendEvent(aBrowser, "SSTabInputCaptured");
       return;
     }
 
     // Store the form data.
     let content = aBrowser.contentWindow;
-    let [formdata] = Utils.mapFrameTree(content, FormData.collect);
+    let [formdata] = Utils.mapFrameTree(content, SessionStoreUtils.collectFormData);
     formdata = PrivacyFilter.filterFormData(formdata || {});
 
     // If we found any form data, main content or frames, let's save it
     if (formdata && Object.keys(formdata).length) {
       data.formdata = formdata;
       log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
       this.saveStateDelayed();
     }
--- a/toolkit/components/sessionstore/SessionStoreUtils.cpp
+++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp
@@ -1,19 +1,28 @@
 /* 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 "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/HTMLTextAreaElement.h"
+#include "mozilla/dom/SessionStoreUtils.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsContentList.h"
+#include "nsContentUtils.h"
+#include "nsFocusManager.h"
+#include "nsGlobalWindowOuter.h"
 #include "nsIDocShell.h"
-#include "nsGlobalWindowOuter.h"
+#include "nsIFormControl.h"
 #include "nsIScrollableFrame.h"
 #include "nsPresContext.h"
-#include "nsCharSeparatedTokenizer.h"
 #include "nsPrintfCString.h"
-#include "mozilla/dom/SessionStoreUtils.h"
+#include "nsTArrayHelpers.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 namespace {
 
 class DynamicFrameEventFilter final : public nsIDOMEventListener {
  public:
@@ -264,8 +273,390 @@ SessionStoreUtils::AddDynamicFrameFilter
 
   nsCCharSeparatedTokenizer tokenizer(aData.mScroll.Value(), ',');
   nsAutoCString token(tokenizer.nextToken());
   int pos_X = atoi(token.get());
   token = tokenizer.nextToken();
   int pos_Y = atoi(token.get());
   aWindow.ScrollTo(pos_X, pos_Y);
 }
+
+// Implements the Luhn checksum algorithm as described at
+// http://wikipedia.org/wiki/Luhn_algorithm
+// Number digit lengths vary with network, but should fall within 12-19 range.
+// [2] More details at https://en.wikipedia.org/wiki/Payment_card_number
+static bool IsValidCCNumber(nsAString& aValue) {
+  uint32_t total = 0;
+  uint32_t numLength = 0;
+  uint32_t strLen = aValue.Length();
+  for (uint32_t i = 0; i < strLen; ++i) {
+    uint32_t idx = strLen - i - 1;
+    // ignore whitespace and dashes)
+    char16_t chr = aValue[idx];
+    if (IsSpaceCharacter(chr) || chr == '-') {
+      continue;
+    }
+    // If our number is too long, note that fact
+    ++numLength;
+    if (numLength > 19) {
+      return false;
+    }
+    // Try to parse the character as a base-10 integer.
+    nsresult rv = NS_OK;
+    uint32_t val = Substring(aValue, idx, 1).ToInteger(&rv, 10);
+    if (NS_FAILED(rv)) {
+      return false;
+    }
+    if (i % 2 == 1) {
+      val *= 2;
+      if (val > 9) {
+        val -= 9;
+      }
+    }
+    total += val;
+  }
+
+  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, OwningStringOrBooleanOrLongOrObject>::EntryType*
+AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId,
+                           uint16_t& aGeneratedCount,
+                           CollectedFormData& aRetVal) {
+  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 {
+    if (!aRetVal.mXpath.WasPassed()) {
+      aRetVal.mXpath.Construct();
+    }
+    auto& recordEntries = aRetVal.mXpath.Value().Entries();
+    entry = recordEntries.AppendElement();
+    nsAutoString xpath;
+    aNode->GenerateXPath(xpath);
+    aGeneratedCount++;
+    entry->mKey = xpath;
+  }
+  return entry;
+}
+
+/*
+  @param aDocument: DOMDocument instance to obtain form data for.
+  @param aGeneratedCount: the current number of XPath expressions in the
+                          returned object.
+  @return aRetVal: Form data encoded in an object.
+ */
+static void CollectFromTextAreaElement(Document& aDocument,
+                                       uint16_t& aGeneratedCount,
+                                       CollectedFormData& aRetVal) {
+  RefPtr<nsContentList> textlist = NS_GetContentList(
+      &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("textarea"));
+  uint32_t length = textlist->Length(true);
+  for (uint32_t i = 0; i < length; ++i) {
+    MOZ_ASSERT(textlist->Item(i), "null item in node list!");
+
+    HTMLTextAreaElement* textArea =
+        HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i));
+    if (!textArea) {
+      continue;
+    }
+    DOMString autocomplete;
+    textArea->GetAutocomplete(autocomplete);
+    if (autocomplete.AsAString().EqualsLiteral("off")) {
+      continue;
+    }
+    nsAutoString id;
+    textArea->GetId(id);
+    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
+      continue;
+    }
+    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, 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
+                          returned object.
+  @return aRetVal: Form data encoded in an object.
+ */
+static void CollectFromInputElement(JSContext* aCx, Document& aDocument,
+                                    uint16_t& aGeneratedCount,
+                                    CollectedFormData& aRetVal) {
+  RefPtr<nsContentList> inputlist = NS_GetContentList(
+      &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("input"));
+  uint32_t length = inputlist->Length(true);
+  for (uint32_t i = 0; i < length; ++i) {
+    MOZ_ASSERT(inputlist->Item(i), "null item in node list!");
+    nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(inputlist->Item(i));
+    if (formControl) {
+      uint8_t controlType = formControl->ControlType();
+      if (controlType == NS_FORM_INPUT_PASSWORD ||
+          controlType == NS_FORM_INPUT_HIDDEN ||
+          controlType == NS_FORM_INPUT_BUTTON ||
+          controlType == NS_FORM_INPUT_IMAGE ||
+          controlType == NS_FORM_INPUT_SUBMIT ||
+          controlType == NS_FORM_INPUT_RESET) {
+        continue;
+      }
+    }
+    RefPtr<HTMLInputElement> input =
+        HTMLInputElement::FromNodeOrNull(inputlist->Item(i));
+    if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) {
+      continue;
+    }
+    nsAutoString id;
+    input->GetId(id);
+    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
+      continue;
+    }
+    Nullable<AutocompleteInfo> aInfo;
+    input->GetAutocompleteInfo(aInfo);
+    if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) {
+      continue;
+    }
+    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, 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;
+      }
+      CollectedFileListValue val;
+      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, 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) ||
+          input->HasBeenTypePassword() ||
+          input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, eCaseMatters)) {
+        continue;
+      }
+      if (!id.IsEmpty()) {
+        // 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 (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, OwningStringOrBooleanOrLongOrObject>::EntryType*
+                  entry = AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal);
+              entry->mValue.SetAsObject() = &jsval.toObject();
+            } else {
+              JS_ClearPendingException(aCx);
+            }
+            continue;
+          }
+        }
+      }
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
+          AppendEntryToCollectedData(input, 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
+                          returned object.
+  @return aRetVal: Form data encoded in an object.
+ */
+static void CollectFromSelectElement(JSContext* aCx, Document& aDocument,
+                                     uint16_t& aGeneratedCount,
+                                     CollectedFormData& aRetVal) {
+  RefPtr<nsContentList> selectlist = NS_GetContentList(
+      &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("select"));
+  uint32_t length = selectlist->Length(true);
+  for (uint32_t i = 0; i < length; ++i) {
+    MOZ_ASSERT(selectlist->Item(i), "null item in node list!");
+    RefPtr<HTMLSelectElement> select =
+        HTMLSelectElement::FromNodeOrNull(selectlist->Item(i));
+    if (!select) {
+      continue;
+    }
+    nsAutoString id;
+    select->GetId(id);
+    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
+      continue;
+    }
+    AutocompleteInfo aInfo;
+    select->GetAutocompleteInfo(aInfo);
+    if (!aInfo.mCanAutomaticallyPersist) {
+      continue;
+    }
+    nsAutoCString value;
+    if (!select->Multiple()) {
+      // <select>s without the multiple attribute are hard to determine the
+      // default value, so assume we don't have the default.
+      DOMString selectVal;
+      select->GetValue(selectVal);
+      CollectedNonMultipleSelectValue val;
+      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, 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;
+      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());
+      }
+      // 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;
+      }
+      JS::Rooted<JS::Value> jsval(aCx);
+      if (!ToJSValue(aCx, selectslist, &jsval)) {
+        JS_ClearPendingException(aCx);
+        continue;
+      }
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
+          AppendEntryToCollectedData(select, id, aGeneratedCount, aRetVal);
+      entry->mValue.SetAsObject() = &jsval.toObject();
+    }
+  }
+}
+
+/*
+  @param aDocument: DOMDocument instance to obtain form data for.
+  @return aRetVal: Form data encoded in an object.
+ */
+static void CollectFromXULTextbox(Document& aDocument,
+                                  CollectedFormData& aRetVal) {
+  nsAutoCString url;
+  Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
+  if (!url.EqualsLiteral("about:config")) {
+    return;
+  }
+  RefPtr<nsContentList> aboutConfigElements = NS_GetContentList(
+      &aDocument, kNameSpaceID_XUL, NS_LITERAL_STRING("window"));
+  uint32_t length = aboutConfigElements->Length(true);
+  for (uint32_t i = 0; i < length; ++i) {
+    MOZ_ASSERT(aboutConfigElements->Item(i), "null item in node list!");
+    nsAutoString id, value;
+    aboutConfigElements->Item(i)->AsElement()->GetId(id);
+    if (!id.EqualsLiteral("config")) {
+      continue;
+    }
+    RefPtr<nsContentList> textboxs =
+        NS_GetContentList(aboutConfigElements->Item(i)->AsElement(),
+                          kNameSpaceID_XUL, NS_LITERAL_STRING("textbox"));
+    uint32_t boxsLength = textboxs->Length(true);
+    for (uint32_t idx = 0; idx < boxsLength; idx++) {
+      textboxs->Item(idx)->AsElement()->GetId(id);
+      if (!id.EqualsLiteral("textbox")) {
+        continue;
+      }
+      RefPtr<HTMLInputElement> input = HTMLInputElement::FromNode(
+          nsFocusManager::GetRedirectedFocus(textboxs->Item(idx)));
+      if (!input) {
+        continue;
+      }
+      input->GetValue(value, CallerType::System);
+      if (value.IsEmpty() ||
+          input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, eCaseMatters)) {
+        continue;
+      }
+      uint16_t generatedCount = 0;
+      Record<nsString, OwningStringOrBooleanOrLongOrObject>::EntryType* entry =
+          AppendEntryToCollectedData(input, id, generatedCount, aRetVal);
+      entry->mValue.SetAsString() = value;
+      return;
+    }
+  }
+}
+
+/* static */ void SessionStoreUtils::CollectFormData(
+    const GlobalObject& aGlobal, Document& aDocument, CollectedFormData& aRetVal) {
+  uint16_t generatedCount = 0;
+  /* textarea element */
+  CollectFromTextAreaElement(aDocument, generatedCount, aRetVal);
+  /* input element */
+  CollectFromInputElement(aGlobal.Context(), aDocument, generatedCount, aRetVal);
+  /* select element */
+  CollectFromSelectElement(aGlobal.Context(), aDocument, generatedCount, aRetVal);
+  /* special case for about:config's search field */
+  CollectFromXULTextbox(aDocument, aRetVal);
+
+  Element* bodyElement = aDocument.GetBody();
+  if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
+    bodyElement->GetInnerHTML(aRetVal.mInnerHTML.Construct(), IgnoreErrors());
+  }
+  if (!aRetVal.mId.WasPassed() && !aRetVal.mXpath.WasPassed() &&
+      !aRetVal.mInnerHTML.WasPassed()) {
+    return;
+  }
+  // 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());
+  }
+}
--- a/toolkit/components/sessionstore/SessionStoreUtils.h
+++ b/toolkit/components/sessionstore/SessionStoreUtils.h
@@ -46,14 +46,17 @@ class SessionStoreUtils {
 
   static void CollectScrollPosition(const GlobalObject& aGlobal,
                                     Document& aDocument,
                                     SSScrollPositionDict& aRetVal);
 
   static void RestoreScrollPosition(const GlobalObject& aGlobal,
                                     nsGlobalWindowInner& aWindow,
                                     const SSScrollPositionDict& data);
+
+  static void CollectFormData(const GlobalObject& aGlobal, Document& aDocument,
+                              CollectedFormData& aRetVal);
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_SessionStoreUtils_h
--- a/toolkit/modules/sessionstore/FormData.jsm
+++ b/toolkit/modules/sessionstore/FormData.jsm
@@ -1,19 +1,16 @@
 /* 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"];
 
-ChromeUtils.defineModuleGetter(this, "CreditCard",
-  "resource://gre/modules/CreditCard.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";
 }
 
@@ -32,41 +29,21 @@ function hasRestorationData(data) {
 /**
  * Returns the given document's current URI and strips
  * off the URI's anchor part, if any.
  */
 function getDocumentURI(doc) {
   return doc.documentURI.replace(/#.*$/, "");
 }
 
-// For a comprehensive list of all available <INPUT> types see
-// https://dxr.mozilla.org/mozilla-central/search?q=kInputTypeTable&redirect=false
-const IGNORE_PROPERTIES = [
-  ["type", new Set(["password", "hidden", "button", "image", "submit", "reset"])],
-  ["autocomplete", new Set(["off"])],
-];
-function shouldIgnoreNode(node) {
-  for (let i = 0; i < IGNORE_PROPERTIES.length; ++i) {
-    let [propName, propValues] = IGNORE_PROPERTIES[i];
-    if (node[propName] && propValues.has(node[propName])) {
-      return true;
-    }
-  }
-  return false;
-}
-
 /**
  * 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({
-  collect(frame) {
-    return FormDataInternal.collect(frame);
-  },
-
   restore(frame, data) {
     return FormDataInternal.restore(frame, data);
   },
 
   restoreTree(root, data) {
     FormDataInternal.restoreTree(root, data);
   },
 });
@@ -105,158 +82,16 @@ var FormDataInternal = {
       // Special case for about:config's search field.
       "|/xul:window[@id='config']//xul:textbox[@id='textbox']";
 
     delete this.restorableFormNodesXPath;
     return (this.restorableFormNodesXPath = formNodesXPath);
   },
 
   /**
-   * 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(doc) {
-    let formNodes = doc.evaluate(
-      this.restorableFormNodesXPath,
-      doc,
-      this.resolveNS.bind(this),
-      doc.defaultView.XPathResult.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())) {
-      if (shouldIgnoreNode(node)) {
-        continue;
-      }
-      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;
-      }
-
-      // We do not want to collect credit card numbers or past/current password fields.
-      if (ChromeUtils.getClassName(node) === "HTMLInputElement") {
-        if (CreditCard.isValidNumber(node.value) || node.hasBeenTypePassword) {
-          continue;
-        }
-      }
-
-      // We don't want to collect values from sensitive fields (indicated by the 'autocomplete'
-      // attribute on relevant elements e.g. autocomplete=off).
-      if (node.getAutocompleteInfo) {
-        let autocompleteInfo = node.getAutocompleteInfo();
-        if (autocompleteInfo && !autocompleteInfo.canAutomaticallyPersist) {
-          continue;
-        }
-      }
-
-      if (ChromeUtils.getClassName(node) === "HTMLInputElement" ||
-          ChromeUtils.getClassName(node) === "HTMLTextAreaElement" ||
-          (node.namespaceURI == this.namespaceURIs.xul && node.localName == "textbox")) {
-        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[node.generateXPath()] = value;
-      }
-    }
-
-    // designMode is undefined e.g. for XUL documents (as about:config)
-    if ((doc.designMode || "") == "on" && doc.body) {
-      // eslint-disable-next-line no-unsanitized/property
-      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.
    */