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