mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 09:15:35 +00:00
58400124a1
Differential Revision: https://phabricator.services.mozilla.com/D12112 --HG-- extra : moz-landing-system : lando
281 lines
8.3 KiB
JavaScript
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]);
|
|
}
|
|
}
|
|
},
|
|
};
|