/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = ["FormData"]; const Cu = Components.utils; const Ci = Components.interfaces; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /** * Returns whether the given URL very likely has input * fields that contain serialized session store data. */ function isRestorationPage(url) { return url == "about:sessionrestore" || url == "about:welcomeback"; } /** * Returns whether the given form |data| object contains nested restoration * data for a page like about:sessionrestore or about:welcomeback. */ function hasRestorationData(data) { if (isRestorationPage(data.url) && data.id) { return typeof(data.id.sessionData) == "object"; } return false; } /** * Returns the given document's current URI and strips * off the URI's anchor part, if any. */ function getDocumentURI(doc) { return doc.documentURI.replace(/#.*$/, ""); } /** * Returns whether the given value is a valid credit card number based on * the Luhn algorithm. See https://en.wikipedia.org/wiki/Luhn_algorithm. */ function isValidCCNumber(value) { // Remove dashes and whitespace. let ccNumber = value.replace(/[-\s]+/g, ""); // Check for non-alphanumeric characters. if (/[^0-9]/.test(ccNumber)) { return false; } // Check for invalid length. let length = ccNumber.length; if (length != 9 && length != 15 && length != 16) { return false; } let total = 0; for (let i = 0; i < length; i++) { let currentChar = ccNumber.charAt(length - i - 1); let currentDigit = parseInt(currentChar, 10); if (i % 2) { // Double every other value. total += currentDigit * 2; // If the doubled value has two digits, add the digits together. if (currentDigit > 4) { total -= 9; } } else { total += currentDigit; } } return total % 10 == 0; } // For a comprehensive list of all available types see // https://dxr.mozilla.org/mozilla-central/search?q=kInputTypeTable&redirect=false const IGNORE_ATTRIBUTES = [ ["type", new Set(["password", "hidden", "button", "image", "submit", "reset"])], ["autocomplete", new Set(["off"])] ]; function shouldIgnoreNode(node) { for (let i = 0; i < IGNORE_ATTRIBUTES.length; ++i) { let [attrName, attrValues] = IGNORE_ATTRIBUTES[i]; if (node.hasAttribute(attrName) && attrValues.has(node.getAttribute(attrName).toLowerCase())) { 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. */ this.FormData = Object.freeze({ collect(frame) { return FormDataInternal.collect(frame); }, 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 = Components.interfaces.nsIDOMXPathResult.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); }, /** * 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({document: doc}) { let formNodes = doc.evaluate( this.restorableFormNodesXPath, doc, this.resolveNS.bind(this), Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null ); let node; let ret = {}; // Limit the number of XPath expressions for performance reasons. See // bug 477564. const MAX_TRAVERSED_XPATHS = 100; let generatedCount = 0; while ((node = formNodes.iterateNext())) { 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. if (node instanceof Ci.nsIDOMHTMLInputElement && isValidCCNumber(node.value)) { continue; } if (node instanceof Ci.nsIDOMHTMLInputElement || node instanceof Ci.nsIDOMHTMLTextAreaElement || node instanceof Ci.nsIDOMXULTextBoxElement) { switch (node.type) { case "checkbox": case "radio": value = node.checked; hasDefaultValue = value == node.defaultChecked; break; case "file": value = { type: "file", fileList: node.mozGetFileNameArray() }; hasDefaultValue = !value.fileList.length; break; default: // text, textarea value = node.value; hasDefaultValue = value == node.defaultValue; break; } } else if (!node.multiple) { // s with the multiple attribute are easier to determine the // default value since each