toolkit/modules/sessionstore/FormData.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["FormData"];
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Ci = Components.interfaces;
michael@0 11
michael@0 12 Cu.import("resource://gre/modules/Timer.jsm");
michael@0 13 Cu.import("resource://gre/modules/XPathGenerator.jsm");
michael@0 14
michael@0 15 /**
michael@0 16 * Returns whether the given URL very likely has input
michael@0 17 * fields that contain serialized session store data.
michael@0 18 */
michael@0 19 function isRestorationPage(url) {
michael@0 20 return url == "about:sessionrestore" || url == "about:welcomeback";
michael@0 21 }
michael@0 22
michael@0 23 /**
michael@0 24 * Returns whether the given form |data| object contains nested restoration
michael@0 25 * data for a page like about:sessionrestore or about:welcomeback.
michael@0 26 */
michael@0 27 function hasRestorationData(data) {
michael@0 28 if (isRestorationPage(data.url) && data.id) {
michael@0 29 return typeof(data.id.sessionData) == "object";
michael@0 30 }
michael@0 31
michael@0 32 return false;
michael@0 33 }
michael@0 34
michael@0 35 /**
michael@0 36 * Returns the given document's current URI and strips
michael@0 37 * off the URI's anchor part, if any.
michael@0 38 */
michael@0 39 function getDocumentURI(doc) {
michael@0 40 return doc.documentURI.replace(/#.*$/, "");
michael@0 41 }
michael@0 42
michael@0 43 /**
michael@0 44 * The public API exported by this module that allows to collect
michael@0 45 * and restore form data for a document and its subframes.
michael@0 46 */
michael@0 47 this.FormData = Object.freeze({
michael@0 48 collect: function (frame) {
michael@0 49 return FormDataInternal.collect(frame);
michael@0 50 },
michael@0 51
michael@0 52 restore: function (frame, data) {
michael@0 53 FormDataInternal.restore(frame, data);
michael@0 54 },
michael@0 55
michael@0 56 restoreTree: function (root, data) {
michael@0 57 FormDataInternal.restoreTree(root, data);
michael@0 58 }
michael@0 59 });
michael@0 60
michael@0 61 /**
michael@0 62 * This module's internal API.
michael@0 63 */
michael@0 64 let FormDataInternal = {
michael@0 65 /**
michael@0 66 * Collect form data for a given |frame| *not* including any subframes.
michael@0 67 *
michael@0 68 * The returned object may have an "id", "xpath", or "innerHTML" key or a
michael@0 69 * combination of those three. Form data stored under "id" is for input
michael@0 70 * fields with id attributes. Data stored under "xpath" is used for input
michael@0 71 * fields that don't have a unique id and need to be queried using XPath.
michael@0 72 * The "innerHTML" key is used for editable documents (designMode=on).
michael@0 73 *
michael@0 74 * Example:
michael@0 75 * {
michael@0 76 * id: {input1: "value1", input3: "value3"},
michael@0 77 * xpath: {
michael@0 78 * "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
michael@0 79 * "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
michael@0 80 * }
michael@0 81 * }
michael@0 82 *
michael@0 83 * @param doc
michael@0 84 * DOMDocument instance to obtain form data for.
michael@0 85 * @return object
michael@0 86 * Form data encoded in an object.
michael@0 87 */
michael@0 88 collect: function ({document: doc}) {
michael@0 89 let formNodes = doc.evaluate(
michael@0 90 XPathGenerator.restorableFormNodes,
michael@0 91 doc,
michael@0 92 XPathGenerator.resolveNS,
michael@0 93 Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
michael@0 94 );
michael@0 95
michael@0 96 let node;
michael@0 97 let ret = {};
michael@0 98
michael@0 99 // Limit the number of XPath expressions for performance reasons. See
michael@0 100 // bug 477564.
michael@0 101 const MAX_TRAVERSED_XPATHS = 100;
michael@0 102 let generatedCount = 0;
michael@0 103
michael@0 104 while (node = formNodes.iterateNext()) {
michael@0 105 let hasDefaultValue = true;
michael@0 106 let value;
michael@0 107
michael@0 108 // Only generate a limited number of XPath expressions for perf reasons
michael@0 109 // (cf. bug 477564)
michael@0 110 if (!node.id && generatedCount > MAX_TRAVERSED_XPATHS) {
michael@0 111 continue;
michael@0 112 }
michael@0 113
michael@0 114 if (node instanceof Ci.nsIDOMHTMLInputElement ||
michael@0 115 node instanceof Ci.nsIDOMHTMLTextAreaElement ||
michael@0 116 node instanceof Ci.nsIDOMXULTextBoxElement) {
michael@0 117 switch (node.type) {
michael@0 118 case "checkbox":
michael@0 119 case "radio":
michael@0 120 value = node.checked;
michael@0 121 hasDefaultValue = value == node.defaultChecked;
michael@0 122 break;
michael@0 123 case "file":
michael@0 124 value = { type: "file", fileList: node.mozGetFileNameArray() };
michael@0 125 hasDefaultValue = !value.fileList.length;
michael@0 126 break;
michael@0 127 default: // text, textarea
michael@0 128 value = node.value;
michael@0 129 hasDefaultValue = value == node.defaultValue;
michael@0 130 break;
michael@0 131 }
michael@0 132 } else if (!node.multiple) {
michael@0 133 // <select>s without the multiple attribute are hard to determine the
michael@0 134 // default value, so assume we don't have the default.
michael@0 135 hasDefaultValue = false;
michael@0 136 value = { selectedIndex: node.selectedIndex, value: node.value };
michael@0 137 } else {
michael@0 138 // <select>s with the multiple attribute are easier to determine the
michael@0 139 // default value since each <option> has a defaultSelected property
michael@0 140 let options = Array.map(node.options, opt => {
michael@0 141 hasDefaultValue = hasDefaultValue && (opt.selected == opt.defaultSelected);
michael@0 142 return opt.selected ? opt.value : -1;
michael@0 143 });
michael@0 144 value = options.filter(ix => ix > -1);
michael@0 145 }
michael@0 146
michael@0 147 // In order to reduce XPath generation (which is slow), we only save data
michael@0 148 // for form fields that have been changed. (cf. bug 537289)
michael@0 149 if (hasDefaultValue) {
michael@0 150 continue;
michael@0 151 }
michael@0 152
michael@0 153 if (node.id) {
michael@0 154 ret.id = ret.id || {};
michael@0 155 ret.id[node.id] = value;
michael@0 156 } else {
michael@0 157 generatedCount++;
michael@0 158 ret.xpath = ret.xpath || {};
michael@0 159 ret.xpath[XPathGenerator.generate(node)] = value;
michael@0 160 }
michael@0 161 }
michael@0 162
michael@0 163 // designMode is undefined e.g. for XUL documents (as about:config)
michael@0 164 if ((doc.designMode || "") == "on" && doc.body) {
michael@0 165 ret.innerHTML = doc.body.innerHTML;
michael@0 166 }
michael@0 167
michael@0 168 // Return |null| if no form data has been found.
michael@0 169 if (Object.keys(ret).length === 0) {
michael@0 170 return null;
michael@0 171 }
michael@0 172
michael@0 173 // Store the frame's current URL with its form data so that we can compare
michael@0 174 // it when restoring data to not inject form data into the wrong document.
michael@0 175 ret.url = getDocumentURI(doc);
michael@0 176
michael@0 177 // We want to avoid saving data for about:sessionrestore as a string.
michael@0 178 // Since it's stored in the form as stringified JSON, stringifying further
michael@0 179 // causes an explosion of escape characters. cf. bug 467409
michael@0 180 if (isRestorationPage(ret.url)) {
michael@0 181 ret.id.sessionData = JSON.parse(ret.id.sessionData);
michael@0 182 }
michael@0 183
michael@0 184 return ret;
michael@0 185 },
michael@0 186
michael@0 187 /**
michael@0 188 * Restores form |data| for the given frame. The data is expected to be in
michael@0 189 * the same format that FormData.collect() returns.
michael@0 190 *
michael@0 191 * @param frame (DOMWindow)
michael@0 192 * The frame to restore form data to.
michael@0 193 * @param data (object)
michael@0 194 * An object holding form data.
michael@0 195 */
michael@0 196 restore: function ({document: doc}, data) {
michael@0 197 // Don't restore any data for the given frame if the URL
michael@0 198 // stored in the form data doesn't match its current URL.
michael@0 199 if (!data.url || data.url != getDocumentURI(doc)) {
michael@0 200 return;
michael@0 201 }
michael@0 202
michael@0 203 // For about:{sessionrestore,welcomeback} we saved the field as JSON to
michael@0 204 // avoid nested instances causing humongous sessionstore.js files.
michael@0 205 // cf. bug 467409
michael@0 206 if (hasRestorationData(data)) {
michael@0 207 data.id.sessionData = JSON.stringify(data.id.sessionData);
michael@0 208 }
michael@0 209
michael@0 210 if ("id" in data) {
michael@0 211 let retrieveNode = id => doc.getElementById(id);
michael@0 212 this.restoreManyInputValues(data.id, retrieveNode);
michael@0 213 }
michael@0 214
michael@0 215 if ("xpath" in data) {
michael@0 216 let retrieveNode = xpath => XPathGenerator.resolve(doc, xpath);
michael@0 217 this.restoreManyInputValues(data.xpath, retrieveNode);
michael@0 218 }
michael@0 219
michael@0 220 if ("innerHTML" in data) {
michael@0 221 // We know that the URL matches data.url right now, but the user
michael@0 222 // may navigate away before the setTimeout handler runs. We do
michael@0 223 // a simple comparison against savedURL to check for that.
michael@0 224 let savedURL = doc.documentURI;
michael@0 225
michael@0 226 setTimeout(() => {
michael@0 227 if (doc.body && doc.designMode == "on" && doc.documentURI == savedURL) {
michael@0 228 doc.body.innerHTML = data.innerHTML;
michael@0 229 }
michael@0 230 });
michael@0 231 }
michael@0 232 },
michael@0 233
michael@0 234 /**
michael@0 235 * Iterates the given form data, retrieving nodes for all the keys and
michael@0 236 * restores their appropriate values.
michael@0 237 *
michael@0 238 * @param data (object)
michael@0 239 * A subset of the form data as collected by FormData.collect(). This
michael@0 240 * is either data stored under "id" or under "xpath".
michael@0 241 * @param retrieve (function)
michael@0 242 * The function used to retrieve the input field belonging to a key
michael@0 243 * in the given |data| object.
michael@0 244 */
michael@0 245 restoreManyInputValues: function (data, retrieve) {
michael@0 246 for (let key of Object.keys(data)) {
michael@0 247 let input = retrieve(key);
michael@0 248 if (input) {
michael@0 249 this.restoreSingleInputValue(input, data[key]);
michael@0 250 }
michael@0 251 }
michael@0 252 },
michael@0 253
michael@0 254 /**
michael@0 255 * Restores a given form value to a given DOMNode and takes care of firing
michael@0 256 * the appropriate DOM event should the input's value change.
michael@0 257 *
michael@0 258 * @param aNode
michael@0 259 * DOMNode to set form value on.
michael@0 260 * @param aValue
michael@0 261 * Value to set form element to.
michael@0 262 */
michael@0 263 restoreSingleInputValue: function (aNode, aValue) {
michael@0 264 let eventType;
michael@0 265
michael@0 266 if (typeof aValue == "string" && aNode.type != "file") {
michael@0 267 // Don't dispatch an input event if there is no change.
michael@0 268 if (aNode.value == aValue) {
michael@0 269 return;
michael@0 270 }
michael@0 271
michael@0 272 aNode.value = aValue;
michael@0 273 eventType = "input";
michael@0 274 } else if (typeof aValue == "boolean") {
michael@0 275 // Don't dispatch a change event for no change.
michael@0 276 if (aNode.checked == aValue) {
michael@0 277 return;
michael@0 278 }
michael@0 279
michael@0 280 aNode.checked = aValue;
michael@0 281 eventType = "change";
michael@0 282 } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
michael@0 283 // Don't dispatch a change event for no change
michael@0 284 if (aNode.options[aNode.selectedIndex].value == aValue.value) {
michael@0 285 return;
michael@0 286 }
michael@0 287
michael@0 288 // find first option with matching aValue if possible
michael@0 289 for (let i = 0; i < aNode.options.length; i++) {
michael@0 290 if (aNode.options[i].value == aValue.value) {
michael@0 291 aNode.selectedIndex = i;
michael@0 292 eventType = "change";
michael@0 293 break;
michael@0 294 }
michael@0 295 }
michael@0 296 } else if (aValue && aValue.fileList && aValue.type == "file" &&
michael@0 297 aNode.type == "file") {
michael@0 298 aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
michael@0 299 eventType = "input";
michael@0 300 } else if (Array.isArray(aValue) && aNode.options) {
michael@0 301 Array.forEach(aNode.options, function(opt, index) {
michael@0 302 // don't worry about malformed options with same values
michael@0 303 opt.selected = aValue.indexOf(opt.value) > -1;
michael@0 304
michael@0 305 // Only fire the event here if this wasn't selected by default
michael@0 306 if (!opt.defaultSelected) {
michael@0 307 eventType = "change";
michael@0 308 }
michael@0 309 });
michael@0 310 }
michael@0 311
michael@0 312 // Fire events for this node if applicable
michael@0 313 if (eventType) {
michael@0 314 let doc = aNode.ownerDocument;
michael@0 315 let event = doc.createEvent("UIEvents");
michael@0 316 event.initUIEvent(eventType, true, true, doc.defaultView, 0);
michael@0 317 aNode.dispatchEvent(event);
michael@0 318 }
michael@0 319 },
michael@0 320
michael@0 321 /**
michael@0 322 * Restores form data for the current frame hierarchy starting at |root|
michael@0 323 * using the given form |data|.
michael@0 324 *
michael@0 325 * If the given |root| frame's hierarchy doesn't match that of the given
michael@0 326 * |data| object we will silently discard data for unreachable frames. For
michael@0 327 * security reasons we will never restore form data to the wrong frames as
michael@0 328 * we bail out silently if the stored URL doesn't match the frame's current
michael@0 329 * URL.
michael@0 330 *
michael@0 331 * @param root (DOMWindow)
michael@0 332 * @param data (object)
michael@0 333 * {
michael@0 334 * formdata: {id: {input1: "value1"}},
michael@0 335 * children: [
michael@0 336 * {formdata: {id: {input2: "value2"}}},
michael@0 337 * null,
michael@0 338 * {formdata: {xpath: { ... }}, children: [ ... ]}
michael@0 339 * ]
michael@0 340 * }
michael@0 341 */
michael@0 342 restoreTree: function (root, data) {
michael@0 343 // Don't restore any data for the root frame and its subframes if there
michael@0 344 // is a URL stored in the form data and it doesn't match its current URL.
michael@0 345 if (data.url && data.url != getDocumentURI(root.document)) {
michael@0 346 return;
michael@0 347 }
michael@0 348
michael@0 349 if (data.url) {
michael@0 350 this.restore(root, data);
michael@0 351 }
michael@0 352
michael@0 353 if (!data.hasOwnProperty("children")) {
michael@0 354 return;
michael@0 355 }
michael@0 356
michael@0 357 let frames = root.frames;
michael@0 358 for (let index of Object.keys(data.children)) {
michael@0 359 if (index < frames.length) {
michael@0 360 this.restoreTree(frames[index], data.children[index]);
michael@0 361 }
michael@0 362 }
michael@0 363 }
michael@0 364 };

mercurial