/* 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) {
//