diff --git a/toolkit/components/satchel/jar.mn b/toolkit/components/satchel/jar.mn new file mode 100644 index 00000000000..4548ab0d404 --- /dev/null +++ b/toolkit/components/satchel/jar.mn @@ -0,0 +1,3 @@ +toolkit.jar: +% content satchel %content/satchel/ +* content/satchel/formSubmitListener.js (src/formSubmitListener.js) diff --git a/toolkit/components/satchel/src/formSubmitListener.js b/toolkit/components/satchel/src/formSubmitListener.js new file mode 100644 index 00000000000..b362d4e9611 --- /dev/null +++ b/toolkit/components/satchel/src/formSubmitListener.js @@ -0,0 +1,199 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske + * Paul O’Shannessy + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const satchelFormListener = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver, + Ci.nsObserver, + Ci.nsISupportsWeakReference]), + + debug : true, + enabled : true, + saveHttpsForms : true, + + init : function() { + Services.obs.addObserver(this, "earlyformsubmit", false); + + let prefBranch = Services.prefs.getBranch("browser.formfill."); + prefBranch.QueryInterface(Ci.nsIPrefBranch2); + prefBranch.addObserver("", this, true); + + this.updatePrefs(); + }, + + updatePrefs : function () { + let prefBranch = Services.prefs.getBranch("browser.formfill."); + this.debug = prefBranch.getBoolPref("debug"); + this.enabled = prefBranch.getBoolPref("enable"); + this.saveHttpsForms = prefBranch.getBoolPref("saveHttpsForms"); + }, + + // Implements the Luhn checksum algorithm as described at + // http://wikipedia.org/wiki/Luhn_algorithm + isValidCCNumber : function (ccNumber) { + // Remove dashes and whitespace + ccNumber = ccNumber.replace(/[\-\s]/g, ''); + + let len = ccNumber.length; + if (len != 9 && len != 15 && len != 16) + return false; + + if (!/^\d+$/.test(ccNumber)) + return false; + + let total = 0; + for (let i = 0; i < len; i++) { + let ch = parseInt(ccNumber[len - i - 1]); + if (i % 2 == 1) { + // Double it, add digits together if > 10 + ch *= 2; + if (ch > 9) + ch -= 9; + } + total += ch; + } + return total % 10 == 0; + }, + + log : function (message) { + if (!this.debug) + return; + dump("satchelFormListener: " + message + "\n"); + Services.console.logStringMessage("satchelFormListener: " + message); + }, + + /* ---- nsIObserver interface ---- */ + + observe : function (subject, topic, data) { + if (topic == "nsPref:changed") + this.updatePrefs(); + else + this.log("Oops! Unexpected notification: " + topic); + }, + + /* ---- nsIFormSubmitObserver interfaces ---- */ + + notify : function(form, domWin, actionURI, cancelSubmit) { + try { + // Even though the global context is for a specific browser, we + // can receive observer events from other tabs! Ensure this event + // is about our content. + if (domWin.top != content) + return; + if (!this.enabled) + return; + + + this.log("Form submit observer notified."); + + if (!this.saveHttpsForms) { + if (actionURI.schemeIs("https")) + return; + if (form.ownerDocument.documentURIObject.schemeIs("https")) + return; + } + + if (form.hasAttribute("autocomplete") && + form.getAttribute("autocomplete").toLowerCase() == "off") + return; + + let entries = []; + for (let i = 0; i < form.elements.length; i++) { + let input = form.elements[i]; + if (!(input instanceof Ci.nsIDOMHTMLInputElement)) + continue; + + // Only use inputs that hold text values (not including type="password") + if (!input.mozIsTextField(true)) + continue; + + // Bug 394612: If Login Manager marked this input, don't save it. + // The login manager will deal with remembering it. + + // Don't save values when autocomplete=off is present. + if (input.hasAttribute("autocomplete") && + input.getAttribute("autocomplete").toLowerCase() == "off") + continue; + + let value = input.value.trim(); + + // Don't save empty or unchanged values. + if (!value || value == input.defaultValue.trim()) + continue; + + // Don't save credit card numbers. + if (this.isValidCCNumber(value)) { + this.log("skipping saving a credit card number"); + continue; + } + + let name = input.name || input.id; + if (!name) + continue; + + // Limit stored data to 200 characters. + if (name.length > 200 || value.length > 200) { + this.log("skipping input that has a name/value too large"); + continue; + } + + // Limit number of fields stored per form. + if (entries.length >= 100) { + this.log("not saving any more entries for this form."); + break; + } + + entries.push({ name: name, value: value }); + } + + if (entries.length) { + this.log("sending entries to parent process for form " + form.id); + sendAsyncMessage("FormHistory:FormSubmitEntries", entries); + } + } + catch (e) { + this.log("notify failed: " + e); + } + } +}; + +satchelFormListener.init(); diff --git a/toolkit/components/satchel/src/nsFormHistory.js b/toolkit/components/satchel/src/nsFormHistory.js index ce581c11661..c0a8bf6f10f 100644 --- a/toolkit/components/satchel/src/nsFormHistory.js +++ b/toolkit/components/satchel/src/nsFormHistory.js @@ -52,12 +52,14 @@ function FormHistory() { FormHistory.prototype = { classID : Components.ID("{0c1bb408-71a2-403f-854a-3a0659829ded}"), - QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2, Ci.nsIObserver, Ci.nsIFormSubmitObserver, Ci.nsISupportsWeakReference]), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2, + Ci.nsIObserver, + Ci.nsIFrameMessageListener, + Ci.nsISupportsWeakReference]), debug : true, enabled : true, saveHttpsForms : true, - prefBranch : null, // The current database schema. dbSchema : { @@ -124,15 +126,21 @@ FormHistory.prototype = { init : function() { let self = this; - this.prefBranch = Services.prefs.getBranch("browser.formfill."); - this.prefBranch.QueryInterface(Ci.nsIPrefBranch2); - this.prefBranch.addObserver("", this, true); + + let prefBranch = Services.prefs.getBranch("browser.formfill."); + prefBranch = prefBranch.QueryInterface(Ci.nsIPrefBranch2); + prefBranch.addObserver("", this, true); + this.updatePrefs(); this.dbStmts = {}; + this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIChromeFrameMessageManager); + this.messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true); + this.messageManager.addMessageListener("FormHistory:FormSubmitEntries", this); + // Add observers - Services.obs.addObserver(this, "earlyformsubmit", false); Services.obs.addObserver(function() { self.expireOldEntries() }, "idle-daily", false); Services.obs.addObserver(function() { self.expireOldEntries() }, "formhistory-expire-now", false); @@ -155,6 +163,26 @@ FormHistory.prototype = { }, + /* ---- message listener ---- */ + + + receiveMessage: function receiveMessage(message) { + // Open a transaction so multiple adds happen in one commit + this.dbConnection.beginTransaction(); + + try { + let entries = message.json; + for (let i = 0; i < entries.length; i++) { + this.addEntry(entries[i].name, entries[i].value); + } + } finally { + // Don't need it to be atomic if there was an error. Commit what + // we managed to put in the table. + this.dbConnection.commitTransaction(); + } + }, + + /* ---- nsIFormHistory2 interfaces ---- */ @@ -173,10 +201,10 @@ FormHistory.prototype = { let now = Date.now() * 1000; // microseconds let [id, guid] = this.getExistingEntryID(name, value); + let stmt; if (id != -1) { // Update existing entry - let stmt; let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id"; let params = { lastUsed : now, @@ -360,87 +388,6 @@ FormHistory.prototype = { }, - /* ---- nsIFormSubmitObserver interfaces ---- */ - - - notify : function(form, domWin, actionURI, cancelSubmit) { - if (!this.enabled) - return; - - this.log("Form submit observer notified."); - - if (!this.saveHttpsForms) { - if (actionURI.schemeIs("https")) - return; - if (form.ownerDocument.documentURIObject.schemeIs("https")) - return; - } - - if (form.hasAttribute("autocomplete") && - form.getAttribute("autocomplete").toLowerCase() == "off") - return; - - // Open a transaction so that multiple additions are efficient. - this.dbConnection.beginTransaction(); - - try { - let savedCount = 0; - for (let i = 0; i < form.elements.length; i++) { - let input = form.elements[i]; - if (!(input instanceof Ci.nsIDOMHTMLInputElement)) - continue; - - // Only use inputs that hold text values (not including type="password") - if (!input.mozIsTextField(true)) - continue; - - // Bug 394612: If Login Manager marked this input, don't save it. - // The login manager will deal with remembering it. - - // Don't save values when autocomplete=off is present. - if (input.hasAttribute("autocomplete") && - input.getAttribute("autocomplete").toLowerCase() == "off") - continue; - - let value = input.value.trim(); - - // Don't save empty or unchanged values. - if (!value || value == input.defaultValue.trim()) - continue; - - // Don't save credit card numbers. - if (this.isValidCCNumber(value)) { - this.log("skipping saving a credit card number"); - continue; - } - - let name = input.name || input.id; - if (!name) - continue; - - // Limit stored data to 200 characters. - if (name.length > 200 || value.length > 200) { - this.log("skipping input that has a name/value too large"); - continue; - } - - // Limit number of fields stored per form. - if (savedCount++ >= 100) { - this.log("not saving any more entries for this form."); - break; - } - - this.addEntry(name, value); - } - } catch (e) { - // Empty - } finally { - // Save whatever we've added so far. - this.dbConnection.commitTransaction(); - } - }, - - /* ---- helpers ---- */ @@ -564,7 +511,8 @@ FormHistory.prototype = { // Determine how many days of history we're supposed to keep. let expireDays = 180; try { - expireDays = this.prefBranch.getIntPref("expire_days"); + let prefBranch = Services.prefs.getBranch("browser.formfill."); + expireDays = prefBranch.getIntPref("expire_days"); } catch (e) { /* ignore */ } let expireTime = Date.now() - expireDays * DAY_IN_MS; @@ -603,41 +551,12 @@ FormHistory.prototype = { updatePrefs : function () { - this.debug = this.prefBranch.getBoolPref("debug"); - this.enabled = this.prefBranch.getBoolPref("enable"); - this.saveHttpsForms = this.prefBranch.getBoolPref("saveHttpsForms"); + let prefBranch = Services.prefs.getBranch("browser.formfill."); + this.debug = prefBranch.getBoolPref("debug"); + this.enabled = prefBranch.getBoolPref("enable"); + this.saveHttpsForms = prefBranch.getBoolPref("saveHttpsForms"); }, - - // Implements the Luhn checksum algorithm as described at - // http://wikipedia.org/wiki/Luhn_algorithm - isValidCCNumber : function (ccNumber) { - // Remove dashes and whitespace - ccNumber = ccNumber.replace(/[\-\s]/g, ''); - - let len = ccNumber.length; - if (len != 9 && len != 15 && len != 16) - return false; - - if (!/^\d+$/.test(ccNumber)) - return false; - - let total = 0; - for (let i = 0; i < len; i++) { - let ch = parseInt(ccNumber[len - i - 1]); - if (i % 2 == 1) { - // Double it, add digits together if > 10 - ch *= 2; - if (ch > 9) - ch -= 9; - } - total += ch; - } - return total % 10 == 0; - }, - - - //**************************************************************************// // Database Creation & Access diff --git a/toolkit/components/satchel/test/satchel_common.js b/toolkit/components/satchel/test/satchel_common.js index 43a069a0bef..dc7452df671 100644 --- a/toolkit/components/satchel/test/satchel_common.js +++ b/toolkit/components/satchel/test/satchel_common.js @@ -35,6 +35,11 @@ * * ***** END LICENSE BLOCK ***** */ +netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + /* * $_ * @@ -112,3 +117,60 @@ function cleanUpFormHist() { formhist.removeAllEntries(); } cleanUpFormHist(); + + +var checkObserver = { + verifyStack: [], + callback: null, + + waitForChecks: function(callback) { + if (this.verifyStack.length == 0) + callback(); + else + this.callback = callback; + }, + + observe: function(subject, topic, data) { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + + if (data != "addEntry" && data != "modifyEntry") + return; + ok(this.verifyStack.length > 0, "checking if saved form data was expected"); + + // Make sure that every piece of data we expect to be saved is saved, and no + // more. Here it is assumed that for every entry satchel saves or modifies, a + // message is sent. + // + // We don't actually check the content of the message, but just that the right + // quantity of messages is received. + // - if there are too few messages, test will time out + // - if there are too many messages, test will error out here + // + var expected = this.verifyStack.shift(); + ok(fh.entryExists(expected.name, expected.value), expected.message); + + if (this.verifyStack.length == 0) { + var callback = this.callback; + this.callback = null; + callback(); + } + } +}; + +function checkForSave(name, value, message) { + checkObserver.verifyStack.push({ name : name, value: value, message: message }); +} + + +function getFormSubmitButton(formNum) { + var form = $("form" + formNum); // by id, not name + ok(form != null, "getting form " + formNum); + + // we can't just call form.submit(), because that doesn't seem to + // invoke the form onsubmit handler. + var button = form.firstChild; + while (button && button.type != "submit") { button = button.nextSibling; } + ok(button != null, "getting form submit button"); + + return button; +} diff --git a/toolkit/components/satchel/test/subtst_form_submission_1.html b/toolkit/components/satchel/test/subtst_form_submission_1.html index e45f9e88caf..f7441668a1c 100644 --- a/toolkit/components/satchel/test/subtst_form_submission_1.html +++ b/toolkit/components/satchel/test/subtst_form_submission_1.html @@ -1,9 +1,38 @@ -
+ + + + + + + + +
+
+ + +
+ + + + diff --git a/toolkit/components/satchel/test/test_form_submission.html b/toolkit/components/satchel/test/test_form_submission.html index be0bb01b228..b4eaed6217b 100644 --- a/toolkit/components/satchel/test/test_form_submission.html +++ b/toolkit/components/satchel/test/test_form_submission.html @@ -9,6 +9,7 @@

+