gecko-dev/toolkit/modules/sessionstore/FormData.jsm
2019-01-17 14:56:51 +00:00

281 lines
8.3 KiB
JavaScript

/* 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]);
}
}
},
};