home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 February / PCWorld_2008-02_cd.bin / temacd / songbird / Songbird_0.4_windows-i686.exe / xulrunner / components / nsLoginManager.js < prev    next >
Text File  |  2007-12-07  |  42KB  |  1,183 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is mozilla.org code.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla Corporation.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Justin Dolske <dolske@mozilla.com> (original author)
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37.  
  38. const Cc = Components.classes;
  39. const Ci = Components.interfaces;
  40.  
  41. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  42.  
  43. function LoginManager() {
  44.     this.init();
  45. }
  46.  
  47. LoginManager.prototype = {
  48.  
  49.     classDescription: "LoginManager",
  50.     contractID: "@mozilla.org/login-manager;1",
  51.     classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
  52.     QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
  53.                                             Ci.nsISupportsWeakReference]),
  54.  
  55.  
  56.     /* ---------- private memebers ---------- */
  57.  
  58.  
  59.     __logService : null, // Console logging service, used for debugging.
  60.     get _logService() {
  61.         if (!this.__logService)
  62.             this.__logService = Cc["@mozilla.org/consoleservice;1"]
  63.                                     .getService(Ci.nsIConsoleService);
  64.         return this.__logService;
  65.     },
  66.  
  67.  
  68.     __ioService: null, // IO service for string -> nsIURI conversion
  69.     get _ioService() {
  70.         if (!this.__ioService)
  71.             this.__ioService = Cc["@mozilla.org/network/io-service;1"]
  72.                                     .getService(Ci.nsIIOService);
  73.         return this.__ioService;
  74.     },
  75.  
  76.  
  77.     __formFillService : null, // FormFillController, for username autocompleting
  78.     get _formFillService() {
  79.         if (!this.__formFillService)
  80.             this.__formFillService = Cc[
  81.                                 "@mozilla.org/satchel/form-fill-controller;1"]
  82.                                     .getService(Ci.nsIFormFillController);
  83.         return this.__formFillService;
  84.     },
  85.  
  86.  
  87.     __storage : null, // Storage component which contains the saved logins
  88.     get _storage() {
  89.         if (!this.__storage) {
  90.             this.__storage = Cc["@mozilla.org/login-manager/storage/legacy;1"]
  91.                                 .createInstance(Ci.nsILoginManagerStorage);
  92.             try {
  93.                 this.__storage.init();
  94.             } catch (e) {
  95.                 this.log("Initialization of storage component failed: " + e);
  96.                 this.__storage = null;
  97.             }
  98.         }
  99.  
  100.         return this.__storage;
  101.     },
  102.  
  103.     _prefBranch : null, // Preferences service
  104.     _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
  105.  
  106.     _remember : true,  // mirrors signon.rememberSignons preference
  107.     _debug    : false, // mirrors signon.debug
  108.  
  109.  
  110.     /*
  111.      * init
  112.      *
  113.      * Initialize the Login Manager. Automatically called when service
  114.      * is created.
  115.      *
  116.      * Note: Service created in /browser/base/content/browser.js,
  117.      *       delayedStartup()
  118.      */
  119.     init : function () {
  120.  
  121.         // Cache references to current |this| in utility objects
  122.         this._webProgressListener._domEventListener = this._domEventListener;
  123.         this._webProgressListener._pwmgr = this;
  124.         this._domEventListener._pwmgr    = this;
  125.         this._observer._pwmgr            = this;
  126.  
  127.         // Preferences. Add observer so we get notified of changes.
  128.         this._prefBranch = Cc["@mozilla.org/preferences-service;1"]
  129.             .getService(Ci.nsIPrefService).getBranch("signon.");
  130.         this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
  131.         this._prefBranch.addObserver("", this._observer, false);
  132.  
  133.         // Get current preference values.
  134.         this._debug = this._prefBranch.getBoolPref("debug");
  135.  
  136.         this._remember = this._prefBranch.getBoolPref("rememberSignons");
  137.  
  138.  
  139.         // Get constructor for nsILoginInfo
  140.         this._nsLoginInfo = new Components.Constructor(
  141.             "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
  142.  
  143.  
  144.         // Form submit observer checks forms for new logins and pw changes.
  145.         var observerService = Cc["@mozilla.org/observer-service;1"]
  146.                                 .getService(Ci.nsIObserverService);
  147.         observerService.addObserver(this._observer, "earlyformsubmit", false);
  148.         observerService.addObserver(this._observer, "xpcom-shutdown", false);
  149.  
  150.         // WebProgressListener for getting notification of new doc loads.
  151.         var progress = Cc["@mozilla.org/docloaderservice;1"]
  152.                         .getService(Ci.nsIWebProgress);
  153.         progress.addProgressListener(this._webProgressListener,
  154.                                      Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  155.  
  156.  
  157.     },
  158.  
  159.  
  160.     /*
  161.      * log
  162.      *
  163.      * Internal function for logging debug messages to the Error Console window
  164.      */
  165.     log : function (message) {
  166.         if (!this._debug)
  167.             return;
  168.         dump("Login Manager: " + message + "\n");
  169.         this._logService.logStringMessage("Login Manager: " + message);
  170.     },
  171.  
  172.  
  173.     /* ---------- Utility objects ---------- */
  174.  
  175.  
  176.     /*
  177.      * _observer object
  178.      *
  179.      * Internal utility object, implements the nsIObserver interface.
  180.      * Used to receive notification for: form submission, preference changes.
  181.      */
  182.     _observer : {
  183.         _pwmgr : null,
  184.  
  185.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 
  186.                                                 Ci.nsIFormSubmitObserver,
  187.                                                 Ci.nsISupportsWeakReference]),
  188.  
  189.  
  190.         // nsFormSubmitObserver
  191.         notify : function (formElement, aWindow, actionURI) {
  192.             this._pwmgr.log("observer notified for form submission.");
  193.  
  194.             // We're invoked before the content's |onsubmit| handlers, so we
  195.             // can grab form data before it might be modified (see bug 257781).
  196.  
  197.             try {
  198.                 this._pwmgr._onFormSubmit(formElement);
  199.             } catch (e) {
  200.                 this._pwmgr.log("Caught error in onFormSubmit: " + e);
  201.             }
  202.  
  203.             return true; // Always return true, or form submit will be canceled.
  204.         },
  205.  
  206.         // nsObserver
  207.         observe : function (subject, topic, data) {
  208.  
  209.             if (topic == "nsPref:changed") {
  210.                 var prefName = data;
  211.                 this._pwmgr.log("got change to " + prefName + " preference");
  212.  
  213.                 if (prefName == "debug") {
  214.                     this._pwmgr._debug = 
  215.                         this._pwmgr._prefBranch.getBoolPref("debug");
  216.                 } else if (prefName == "rememberSignons") {
  217.                     this._pwmgr._remember =
  218.                         this._pwmgr._prefBranch.getBoolPref("rememberSignons");
  219.                 } else {
  220.                     this._pwmgr.log("Oops! Pref not handled, change ignored.");
  221.                 }
  222.             } else if (topic == "xpcom-shutdown") {
  223.                 for (let i in this._pwmgr) {
  224.                   try {
  225.                     this._pwmgr[i] = null;
  226.                   } catch(ex) {}
  227.                 }
  228.                 this._pwmgr = null;
  229.             } else {
  230.                 this._pwmgr.log("Oops! Unexpected notification: " + topic);
  231.             }
  232.         }
  233.     },
  234.  
  235.  
  236.     /*
  237.      * _webProgressListener object
  238.      *
  239.      * Internal utility object, implements nsIWebProgressListener interface.
  240.      * This is attached to the document loader service, so we get
  241.      * notifications about all page loads.
  242.      */
  243.     _webProgressListener : {
  244.         _pwmgr : null,
  245.         _domEventListener : null,
  246.  
  247.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
  248.                                                 Ci.nsISupportsWeakReference]),
  249.  
  250.  
  251.         onStateChange : function (aWebProgress, aRequest,
  252.                                   aStateFlags,  aStatus) {
  253.  
  254.             // STATE_START is too early, doc is still the old page.
  255.             if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
  256.                 return;
  257.  
  258.             if (!this._pwmgr._remember)
  259.                 return;
  260.  
  261.             var domWin = aWebProgress.DOMWindow;
  262.             var domDoc = domWin.document;
  263.  
  264.             // Only process things which might have HTML forms.
  265.             if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
  266.                 return;
  267.  
  268.             this._pwmgr.log("onStateChange accepted: req = " + (aRequest ?
  269.                         aRequest.name : "(null)") + ", flags = " + aStateFlags);
  270.  
  271.             // fastback navigation... We won't get a DOMContentLoaded
  272.             // event again, so process any forms now.
  273.             if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
  274.                 this._pwmgr.log("onStateChange: restoring document");
  275.                 return this._pwmgr._fillDocument(domDoc);
  276.             }
  277.  
  278.             // Add event listener to process page when DOM is complete.
  279.             this._pwmgr.log("onStateChange: adding dom listeners");
  280.             domDoc.addEventListener("DOMContentLoaded",
  281.                                     this._domEventListener, false);
  282.             return;
  283.         },
  284.  
  285.         // stubs for the nsIWebProgressListener interfaces which we don't use.
  286.         onProgressChange : function() { throw "Unexpected onProgressChange"; },
  287.         onLocationChange : function() { throw "Unexpected onLocationChange"; },
  288.         onStatusChange   : function() { throw "Unexpected onStatusChange";   },
  289.         onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
  290.     },
  291.  
  292.  
  293.     /*
  294.      * _domEventListener object
  295.      *
  296.      * Internal utility object, implements nsIDOMEventListener
  297.      * Used to catch certain DOM events needed to properly implement form fill.
  298.      */
  299.     _domEventListener : {
  300.         _pwmgr : null,
  301.  
  302.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
  303.                                                 Ci.nsISupportsWeakReference]),
  304.  
  305.  
  306.         handleEvent : function (event) {
  307.             this._pwmgr.log("domEventListener: got event " + event.type);
  308.  
  309.             var doc, inputElement;
  310.             switch (event.type) {
  311.                 case "DOMContentLoaded":
  312.                     doc = event.target;
  313.                     this._pwmgr._fillDocument(doc);
  314.                     return;
  315.  
  316.                 case "DOMAutoComplete":
  317.                 case "blur":
  318.                     inputElement = event.target;
  319.                     this._pwmgr._fillPassword(inputElement);
  320.                     return;
  321.  
  322.                 default:
  323.                     this._pwmgr.log("Oops! This event unexpected.");
  324.                     return;
  325.             }
  326.         }
  327.     },
  328.  
  329.  
  330.  
  331.  
  332.     /* ---------- Primary Public interfaces ---------- */
  333.  
  334.  
  335.  
  336.  
  337.     /*
  338.      * addLogin
  339.      *
  340.      * Add a new login to login storage.
  341.      */
  342.     addLogin : function (login) {
  343.         // Sanity check the login
  344.         if (login.hostname == null || login.hostname.length == 0)
  345.             throw "Can't add a login with a null or empty hostname.";
  346.  
  347.         // For logins w/o a username, set to "", not null.
  348.         if (login.username == null)
  349.             throw "Can't add a login with a null username.";
  350.  
  351.         if (login.password == null || login.password.length == 0)
  352.             throw "Can't add a login with a null or empty password.";
  353.  
  354.         if (!login.httpRealm && !login.formSubmitURL)
  355.             throw "Can't add a login without a httpRealm or formSubmitURL.";
  356.  
  357.         // Look for an existing entry.
  358.         var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
  359.                                      login.httpRealm);
  360.  
  361.         if (logins.some(function(l) { return login.username == l.username }))
  362.             throw "This login already exists.";
  363.  
  364.         this.log("Adding login: " + login);
  365.         return this._storage.addLogin(login);
  366.     },
  367.  
  368.  
  369.     /*
  370.      * removeLogin
  371.      *
  372.      * Remove the specified login from the stored logins.
  373.      */
  374.     removeLogin : function (login) {
  375.         this.log("Removing login: " + login);
  376.         return this._storage.removeLogin(login);
  377.     },
  378.  
  379.  
  380.     /*
  381.      * modifyLogin
  382.      *
  383.      * Change the specified login to match the new login.
  384.      */
  385.     modifyLogin : function (oldLogin, newLogin) {
  386.         this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
  387.         return this._storage.modifyLogin(oldLogin, newLogin);
  388.     },
  389.  
  390.  
  391.     /*
  392.      * getAllLogins
  393.      *
  394.      * Get a dump of all stored logins. Used by the login manager UI.
  395.      *
  396.      * |count| is only needed for XPCOM.
  397.      *
  398.      * Returns an array of logins. If there are no logins, the array is empty.
  399.      */
  400.     getAllLogins : function (count) {
  401.         this.log("Getting a list of all logins");
  402.         return this._storage.getAllLogins(count);
  403.     },
  404.  
  405.  
  406.     /*
  407.      * removeAllLogins
  408.      *
  409.      * Remove all stored logins.
  410.      */
  411.     removeAllLogins : function () {
  412.         this.log("Removing all logins");
  413.         this._storage.removeAllLogins();
  414.     },
  415.  
  416.     /*
  417.      * getAllDisabledHosts
  418.      *
  419.      * Get a list of all hosts for which logins are disabled.
  420.      *
  421.      * |count| is only needed for XPCOM.
  422.      *
  423.      * Returns an array of disabled logins. If there are no disabled logins,
  424.      * the array is empty.
  425.      */
  426.     getAllDisabledHosts : function (count) {
  427.         this.log("Getting a list of all disabled hosts");
  428.         return this._storage.getAllDisabledHosts(count);
  429.     },
  430.  
  431.  
  432.     /*
  433.      * findLogins
  434.      *
  435.      * Search for the known logins for entries matching the specified criteria.
  436.      */
  437.     findLogins : function (count, hostname, formSubmitURL, httpRealm) {
  438.         this.log("Searching for logins matching host: " + hostname +
  439.             ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  440.  
  441.         return this._storage.findLogins(count, hostname, formSubmitURL,
  442.                                         httpRealm);
  443.     },
  444.  
  445.  
  446.     /*
  447.      * countLogins
  448.      *
  449.      * Search for the known logins for entries matching the specified criteria,
  450.      * returns only the count.
  451.      */
  452.     countLogins : function (hostname, formSubmitURL, httpRealm) {
  453.         this.log("Counting logins matching host: " + hostname +
  454.             ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  455.  
  456.         return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
  457.     },
  458.  
  459.  
  460.     /*
  461.      * getLoginSavingEnabled
  462.      *
  463.      * Check to see if user has disabled saving logins for the host.
  464.      */
  465.     getLoginSavingEnabled : function (host) {
  466.         this.log("Checking if logins to " + host + " can be saved.");
  467.         if (!this._remember)
  468.             return false;
  469.  
  470.         return this._storage.getLoginSavingEnabled(host);
  471.     },
  472.  
  473.  
  474.     /*
  475.      * setLoginSavingEnabled
  476.      *
  477.      * Enable or disable storing logins for the specified host.
  478.      */
  479.     setLoginSavingEnabled : function (hostname, enabled) {
  480.         this.log("Saving logins for " + hostname + " enabled? " + enabled);
  481.         return this._storage.setLoginSavingEnabled(hostname, enabled);
  482.     },
  483.  
  484.  
  485.     /*
  486.      * autoCompleteSearch
  487.      *
  488.      * Yuck. This is called directly by satchel:
  489.      * nsFormFillController::StartSearch()
  490.      * [toolkit/components/satchel/src/nsFormFillController.cpp]
  491.      *
  492.      * We really ought to have a simple way for code to register an
  493.      * auto-complete provider, and not have satchel calling pwmgr directly.
  494.      */
  495.     autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
  496.         // aPreviousResult & aResult are nsIAutoCompleteResult,
  497.         // aElement is nsIDOMHTMLInputElement
  498.  
  499.         if (!this._remember)
  500.             return false;
  501.  
  502.         this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
  503.  
  504.         var result = null;
  505.  
  506.         if (aPreviousResult) {
  507.             this.log("Using previous autocomplete result");
  508.             result = aPreviousResult;
  509.  
  510.             // We have a list of results for a shorter search string, so just
  511.             // filter them further based on the new search string.
  512.             // Count backwards, because result.matchCount is decremented
  513.             // when we remove an entry.
  514.             for (var i = result.matchCount - 1; i >= 0; i--) {
  515.                 var match = result.getValueAt(i);
  516.  
  517.                 // Remove results that are too short, or have different prefix.
  518.                 if (aSearchString.length > match.length ||
  519.                     aSearchString.toLowerCase() !=
  520.                         match.substr(0, aSearchString.length).toLowerCase())
  521.                 {
  522.                     this.log("Removing autocomplete entry '" + match + "'");
  523.                     result.removeValueAt(i, false);
  524.                 }
  525.             }
  526.         } else {
  527.             this.log("Creating new autocomplete search result.");
  528.  
  529.             var doc = aElement.ownerDocument;
  530.             var origin = this._getPasswordOrigin(doc.documentURI);
  531.             var actionOrigin = this._getActionOrigin(aElement.form);
  532.  
  533.             var logins = this.findLogins({}, origin, actionOrigin, null);
  534.             var matchingLogins = [];
  535.  
  536.             for (i = 0; i < logins.length; i++) {
  537.                 var username = logins[i].username.toLowerCase();
  538.                 if (aSearchString.length <= username.length &&
  539.                     aSearchString.toLowerCase() ==
  540.                         username.substr(0, aSearchString.length))
  541.                 {
  542.                     matchingLogins.push(logins[i]);
  543.                 }
  544.             }
  545.             this.log(matchingLogins.length + " autocomplete logins avail.");
  546.             result = new UserAutoCompleteResult(aSearchString, matchingLogins);
  547.         }
  548.  
  549.         return result;
  550.     },
  551.  
  552.  
  553.  
  554.  
  555.     /* ------- Internal methods / callbacks for document integration ------- */
  556.  
  557.  
  558.  
  559.  
  560.     /*
  561.      * _getPasswordFields
  562.      *
  563.      * Returns an array of password field elements for the specified form.
  564.      * If no pw fields are found, or if more than 3 are found, then null
  565.      * is returned.
  566.      *
  567.      * skipEmptyFields can be set to ignore password fields with no value.
  568.      */
  569.     _getPasswordFields : function (form, skipEmptyFields) {
  570.         // Locate the password fields in the form.
  571.         var pwFields = [];
  572.         for (var i = 0; i < form.elements.length; i++) {
  573.             if (form.elements[i].type != "password")
  574.                 continue;
  575.  
  576.             if (skipEmptyFields && !form.elements[i].value)
  577.                 continue;
  578.  
  579.             pwFields[pwFields.length] = {
  580.                                             index   : i,
  581.                                             element : form.elements[i]
  582.                                         };
  583.         }
  584.  
  585.         // If too few or too many fields, bail out.
  586.         if (pwFields.length == 0) {
  587.             this.log("(form ignored -- no password fields.)");
  588.             return null;
  589.         } else if (pwFields.length > 3) {
  590.             this.log("(form ignored -- too many password fields. [got " +
  591.                         pwFields.length + "])");
  592.             return null;
  593.         }
  594.  
  595.         return pwFields;
  596.     },
  597.  
  598.  
  599.     /*
  600.      * _getFormFields
  601.      *
  602.      * Returns the username and password fields found in the form.
  603.      * Can handle complex forms by trying to figure out what the
  604.      * relevant fields are.
  605.      *
  606.      * Returns: [usernameField, newPasswordField, oldPasswordField]
  607.      *
  608.      * usernameField may be null.
  609.      * newPasswordField will always be non-null.
  610.      * oldPasswordField may be null. If null, newPasswordField is just
  611.      * "theLoginField". If not null, the form is apparently a
  612.      * change-password field, with oldPasswordField containing the password
  613.      * that is being changed.
  614.      */
  615.     _getFormFields : function (form, isSubmission) {
  616.         var usernameField = null;
  617.  
  618.         // Locate the password field(s) in the form. Up to 3 supported.
  619.         // If there's no password field, there's nothing for us to do.
  620.         var pwFields = this._getPasswordFields(form, isSubmission);
  621.         if (!pwFields)
  622.             return [null, null, null];
  623.  
  624.  
  625.         // Locate the username field in the form by searching backwards
  626.         // from the first passwordfield, assume the first text field is the
  627.         // username. We might not find a username field if the user is
  628.         // already logged in to the site. 
  629.         for (var i = pwFields[0].index - 1; i >= 0; i--) {
  630.             if (form.elements[i].type == "text") {
  631.                 usernameField = form.elements[i];
  632.                 break;
  633.             }
  634.         }
  635.  
  636.         if (!usernameField)
  637.             this.log("(form -- no username field found)");
  638.  
  639.  
  640.         // If we're not submitting a form (it's a page load), there are no
  641.         // password field values for us to use for identifying fields. So,
  642.         // just assume the first password field is the one to be filled in.
  643.         if (!isSubmission || pwFields.length == 1)
  644.             return [usernameField, pwFields[0].element, null];
  645.  
  646.  
  647.         // Try to figure out WTF is in the form based on the password values.
  648.         var oldPasswordField, newPasswordField;
  649.         var pw1 = pwFields[0].element.value;
  650.         var pw2 = pwFields[1].element.value;
  651.         var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
  652.  
  653.         if (pwFields.length == 3) {
  654.             // Look for two identical passwords, that's the new password
  655.  
  656.             if (pw1 == pw2 && pw2 == pw3) {
  657.                 // All 3 passwords the same? Weird! Treat as if 1 pw field.
  658.                 newPasswordField = pwFields[0].element;
  659.                 oldPasswordField = null;
  660.             } else if (pw1 == pw2) {
  661.                 newPasswordField = pwFields[0].element;
  662.                 oldPasswordField = pwFields[2].element;
  663.             } else if (pw2 == pw3) {
  664.                 oldPasswordField = pwFields[0].element;
  665.                 newPasswordField = pwFields[2].element;
  666.             } else  if (pw1 == pw3) {
  667.                 // A bit odd, but could make sense with the right page layout.
  668.                 newPasswordField = pwFields[0].element;
  669.                 oldPasswordField = pwFields[1].element;
  670.             } else {
  671.                 // We can't tell which of the 3 passwords should be saved.
  672.                 this.log("(form ignored -- all 3 pw fields differ)");
  673.                 return [null, null, null];
  674.             }
  675.         } else { // pwFields.length == 2
  676.             if (pw1 == pw2) {
  677.                 // Treat as if 1 pw field
  678.                 newPasswordField = pwFields[0].element;
  679.                 oldPasswordField = null;
  680.             } else {
  681.                 // Just assume that the 2nd password is the new password
  682.                 oldPasswordField = pwFields[0].element;
  683.                 newPasswordField = pwFields[1].element;
  684.             }
  685.         }
  686.  
  687.         return [usernameField, newPasswordField, oldPasswordField];
  688.     },
  689.  
  690.  
  691.     /*
  692.      * _onFormSubmit
  693.      *
  694.      * Called by the our observer when notified of a form submission.
  695.      * [Note that this happens before any DOM onsubmit handlers are invoked.]
  696.      * Looks for a password change in the submitted form, so we can update
  697.      * our stored password.
  698.      */
  699.     _onFormSubmit : function (form) {
  700.  
  701.         // local helper function
  702.         function autocompleteDisabled(element) {
  703.             if (element && element.hasAttribute("autocomplete") &&
  704.                 element.getAttribute("autocomplete").toLowerCase() == "off")
  705.                 return true;
  706.  
  707.            return false;
  708.         };
  709.  
  710.         // local helper function
  711.         function getPrompter(aWindow) {
  712.             var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
  713.                             createInstance(Ci.nsILoginManagerPrompter);
  714.             prompterSvc.init(aWindow);
  715.             return prompterSvc;
  716.         }
  717.  
  718.         var doc = form.ownerDocument;
  719.         var win = doc.defaultView;
  720.  
  721.         // If password saving is disabled (globally or for host), bail out now.
  722.         if (!this._remember)
  723.             return;
  724.  
  725.         var hostname      = this._getPasswordOrigin(doc.documentURI);
  726.         var formSubmitURL = this._getActionOrigin(form)
  727.         if (!this.getLoginSavingEnabled(hostname)) {
  728.             this.log("(form submission ignored -- saving is " +
  729.                      "disabled for: " + hostname + ")");
  730.             return;
  731.         }
  732.  
  733.  
  734.         // Get the appropriate fields from the form.
  735.         var [usernameField, newPasswordField, oldPasswordField] =
  736.             this._getFormFields(form, true);
  737.  
  738.         // Need at least 1 valid password field to do anything.
  739.         if (newPasswordField == null)
  740.                 return;
  741.  
  742.         // Check for autocomplete=off attribute. We don't use it to prevent
  743.         // autofilling (for existing logins), but won't save logins when it's
  744.         // present.
  745.         if (autocompleteDisabled(form) ||
  746.             autocompleteDisabled(usernameField) ||
  747.             autocompleteDisabled(newPasswordField) ||
  748.             autocompleteDisabled(oldPasswordField)) {
  749.                 this.log("(form submission ignored -- autocomplete=off found)");
  750.                 return;
  751.         }
  752.  
  753.  
  754.         var formLogin = new this._nsLoginInfo();
  755.         formLogin.init(hostname, formSubmitURL, null,
  756.                     (usernameField ? usernameField.value : ""),
  757.                     newPasswordField.value,
  758.                     (usernameField ? usernameField.name  : ""),
  759.                     newPasswordField.name);
  760.  
  761.         // If we didn't find a username field, but seem to be changing a
  762.         // password, allow the user to select from a list of applicable
  763.         // logins to update the password for.
  764.         if (!usernameField && oldPasswordField) {
  765.  
  766.             var logins = this.findLogins({}, hostname, formSubmitURL, null);
  767.  
  768.             if (logins.length == 0) {
  769.                 // Could prompt to save this as a new password-only login.
  770.                 // This seems uncommon, and might be wrong, so ignore.
  771.                 this.log("(no logins for this host -- pwchange ignored)");
  772.                 return;
  773.             }
  774.  
  775.             var prompter = getPrompter(win);
  776.  
  777.             if (logins.length == 1) {
  778.                 var oldLogin = logins[0];
  779.                 formLogin.username      = oldLogin.username;
  780.                 formLogin.usernameField = oldLogin.usernameField;
  781.  
  782.                 prompter.promptToChangePassword(oldLogin, formLogin);
  783.             } else {
  784.                 prompter.promptToChangePasswordWithUsernames(
  785.                                     logins, logins.length, formLogin);
  786.             }
  787.  
  788.             return;
  789.         }
  790.  
  791.  
  792.         // Look for an existing login that matches the form login.
  793.         var existingLogin = null;
  794.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  795.  
  796.         for (var i = 0; i < logins.length; i++) {
  797.             var same, login = logins[i];
  798.  
  799.             // If one login has a username but the other doesn't, ignore
  800.             // the username when comparing and only match if they have the
  801.             // same password. Otherwise, compare the logins and match even
  802.             // if the passwords differ.
  803.             if (!login.username && formLogin.username) {
  804.                 var restoreMe = formLogin.username;
  805.                 formLogin.username = ""; 
  806.                 same = formLogin.equals(login);
  807.                 formLogin.username = restoreMe;
  808.             } else if (!formLogin.username && login.username) {
  809.                 formLogin.username = login.username;
  810.                 same = formLogin.equals(login);
  811.                 formLogin.username = ""; // we know it's always blank.
  812.             } else {
  813.                 same = formLogin.equalsIgnorePassword(login);
  814.             }
  815.  
  816.             if (same) {
  817.                 existingLogin = login;
  818.                 break;
  819.             }
  820.         }
  821.  
  822.         if (existingLogin) {
  823.             this.log("Found an existing login matching this form submission");
  824.  
  825.             /*
  826.              * Change password if needed.
  827.              *
  828.              * If the login has a username, change the password w/o prompting
  829.              * (because we can be fairly sure there's only one password
  830.              * associated with the username). But for logins without a
  831.              * username, ask the user... Some sites use a password-only "login"
  832.              * in different contexts (enter your PIN, answer a security
  833.              * question, etc), and without a username we can't be sure if
  834.              * modifying an existing login is the right thing to do.
  835.              */
  836.             if (existingLogin.password != formLogin.password) {
  837.                 if (formLogin.username) {
  838.                     this.log("...Updating password for existing login.");
  839.                     this.modifyLogin(existingLogin, formLogin);
  840.                 } else {
  841.                     this.log("...passwords differ, prompting to change.");
  842.                     prompter = getPrompter(win);
  843.                     prompter.promptToChangePassword(existingLogin, formLogin);
  844.                 }
  845.             }
  846.  
  847.             return;
  848.         }
  849.  
  850.  
  851.         // Prompt user to save login (via dialog or notification bar)
  852.         prompter = getPrompter(win);
  853.         prompter.promptToSavePassword(formLogin);
  854.     },
  855.  
  856.  
  857.     /*
  858.      * _getPasswordOrigin
  859.      *
  860.      * Get the parts of the URL we want for identification.
  861.      */
  862.     _getPasswordOrigin : function (uriString) {
  863.         var realm = "";
  864.         try {
  865.             var uri = this._ioService.newURI(uriString, null, null);
  866.  
  867.             realm = uri.scheme + "://" + uri.host;
  868.  
  869.             // If the URI explicitly specified a port, only include it when
  870.             // it's not the default. (We never want "http://foo.com:80")
  871.             var port = uri.port;
  872.             if (port != -1) {
  873.                 var handler = this._ioService.getProtocolHandler(uri.scheme);
  874.                 if (port != handler.defaultPort)
  875.                     realm += ":" + port;
  876.             }
  877.  
  878.         } catch (e) {
  879.             // bug 159484 - disallow url types that don't support a hostPort.
  880.             // (set null to cause throw in the JS above)
  881.             this.log("Couldn't parse origin for " + uriString);
  882.             realm = null;
  883.         }
  884.  
  885.         return realm;
  886.     },
  887.  
  888.     _getActionOrigin : function (form) {
  889.         var uriString = form.action;
  890.  
  891.         // A blank or mission action submits to where it came from.
  892.         if (uriString == "")
  893.             uriString = form.baseURI; // ala bug 297761
  894.  
  895.         return this._getPasswordOrigin(uriString);
  896.     },
  897.  
  898.  
  899.     /*
  900.      * _fillDocument
  901.      *
  902.      * Called when a page has loaded. For each form in the document,
  903.      * we check to see if it can be filled with a stored login.
  904.      */
  905.     _fillDocument : function (doc) {
  906.         var forms = doc.forms;
  907.         if (!forms || forms.length == 0)
  908.             return;
  909.  
  910.         var formOrigin = this._getPasswordOrigin(doc.documentURI);
  911.  
  912.         // If there are no logins for this site, bail out now.
  913.         if (!this.countLogins(formOrigin, "", null))
  914.             return;
  915.  
  916.         this.log("fillDocument processing " + forms.length +
  917.                  " forms on " + doc.documentURI);
  918.  
  919.         var autofillForm = this._prefBranch.getBoolPref("autofillForms");
  920.         var previousActionOrigin = null;
  921.  
  922.         for (var i = 0; i < forms.length; i++) {
  923.             var form = forms[i];
  924.  
  925.             // Heuristically determine what the user/pass fields are
  926.             // We do this before checking to see if logins are stored,
  927.             // so that the user isn't prompted for a master password
  928.             // without need.
  929.             var [usernameField, passwordField, ignored] =
  930.                 this._getFormFields(form, false);
  931.  
  932.             // Need a valid password field to do anything.
  933.             if (passwordField == null)
  934.                 continue;
  935.  
  936.  
  937.             // Only the actionOrigin might be changing, so if it's the same
  938.             // as the last form on the page we can reuse the same logins.
  939.             var actionOrigin = this._getActionOrigin(form);
  940.             if (actionOrigin != previousActionOrigin) {
  941.                 var foundLogins =
  942.                     this.findLogins({}, formOrigin, actionOrigin, null);
  943.  
  944.                 this.log("form[" + i + "]: got " +
  945.                          foundLogins.length + " logins.");
  946.  
  947.                 previousActionOrigin = actionOrigin;
  948.             } else {
  949.                 this.log("form[" + i + "]: using logins from last form.");
  950.             }
  951.  
  952.  
  953.             // Discard logins which have username/password values that don't
  954.             // fit into the fields (as specified by the maxlength attribute).
  955.             // The user couldn't enter these values anyway, and it helps
  956.             // with sites that have an extra PIN to be entered (bug 391514)
  957.             var maxUsernameLen = Number.MAX_VALUE;
  958.             var maxPasswordLen = Number.MAX_VALUE;
  959.  
  960.             // If attribute wasn't set, default is -1.
  961.             if (usernameField && usernameField.maxLength >= 0)
  962.                 maxUsernameLen = usernameField.maxLength;
  963.             if (passwordField.maxLength >= 0)
  964.                 maxPasswordLen = passwordField.maxLength;
  965.  
  966.             logins = foundLogins.filter(function (l) {
  967.                     var fit = (l.username.length <= maxUsernameLen &&
  968.                                l.password.length <= maxPasswordLen);
  969.                     if (!fit)
  970.                         this.log("Ignored " + l.username + " login: won't fit");
  971.  
  972.                     return fit;
  973.                 }, this);
  974.  
  975.  
  976.             // Nothing to do if we have no matching logins available.
  977.             if (logins.length == 0)
  978.                 continue;
  979.  
  980.  
  981.             // Attach autocomplete stuff to the username field, if we have
  982.             // one. This is normally used to select from multiple accounts,
  983.             // but even with one account we should refill if the user edits.
  984.             if (usernameField)
  985.                 this._attachToInput(usernameField);
  986.  
  987.             if (autofillForm) {
  988.  
  989.                 if (usernameField && usernameField.value) {
  990.                     // If username was specified in the form, only fill in the
  991.                     // password if we find a matching login.
  992.  
  993.                     var username = usernameField.value;
  994.  
  995.                     var matchingLogin;
  996.                     var found = logins.some(function(l) {
  997.                                                 matchingLogin = l;
  998.                                                 return (l.username == username);
  999.                                             });
  1000.                     if (found)
  1001.                         passwordField.value = matchingLogin.password;
  1002.  
  1003.                 } else if (usernameField && logins.length == 2) {
  1004.                     // Special case, for sites which have a normal user+pass
  1005.                     // login *and* a password-only login (eg, a PIN)...
  1006.                     // When we have a username field and 1 of 2 available
  1007.                     // logins is password-only, go ahead and prefill the
  1008.                     // one with a username.
  1009.                     if (!logins[0].username && logins[1].username) {
  1010.                         usernameField.value = logins[1].username;
  1011.                         passwordField.value = logins[1].password;
  1012.                     } else if (!logins[1].username && logins[0].username) {
  1013.                         usernameField.value = logins[0].username;
  1014.                         passwordField.value = logins[0].password;
  1015.                     }
  1016.                 } else if (logins.length == 1) {
  1017.                     if (usernameField)
  1018.                         usernameField.value = logins[0].username;
  1019.                     passwordField.value = logins[0].password;
  1020.                 }
  1021.             }
  1022.         } // foreach form
  1023.     },
  1024.  
  1025.  
  1026.     /*
  1027.      * _attachToInput
  1028.      *
  1029.      * Hooks up autocomplete support to a username field, to allow
  1030.      * a user editing the field to select an existing login and have
  1031.      * the password field filled in.
  1032.      */
  1033.     _attachToInput : function (element) {
  1034.         this.log("attaching autocomplete stuff");
  1035.         element.addEventListener("blur",
  1036.                                 this._domEventListener, false);
  1037.         element.addEventListener("DOMAutoComplete",
  1038.                                 this._domEventListener, false);
  1039.         this._formFillService.markAsLoginManagerField(element);
  1040.     },
  1041.  
  1042.  
  1043.     /*
  1044.      * _fillPassword
  1045.      *
  1046.      * The user has autocompleted a username field, so fill in the password.
  1047.      */
  1048.     _fillPassword : function (usernameField) {
  1049.         this.log("fillPassword autocomplete username: " + usernameField.value);
  1050.  
  1051.         var form = usernameField.form;
  1052.         var doc = form.ownerDocument;
  1053.  
  1054.         var hostname = this._getPasswordOrigin(doc.documentURI);
  1055.         var formSubmitURL = this._getActionOrigin(form)
  1056.  
  1057.         // Find the password field. We should always have at least one,
  1058.         // or else something has gone rather wrong.
  1059.         var pwFields = this._getPasswordFields(form, false);
  1060.         if (!pwFields) {
  1061.             const err = "No password field for autocomplete password fill.";
  1062.  
  1063.             // We want to know about this even if debugging is disabled.
  1064.             if (!this._debug)
  1065.                 dump(err);
  1066.             else
  1067.                 this.log(err);
  1068.  
  1069.             return;
  1070.         }
  1071.  
  1072.         // If there are multiple passwords fields, we can't really figure
  1073.         // out what each field is for, so just fill out the last field.
  1074.         var passwordField = pwFields[0].element;
  1075.  
  1076.         // Temporary LoginInfo with the info we know.
  1077.         var currentLogin = new this._nsLoginInfo();
  1078.         currentLogin.init(hostname, formSubmitURL, null,
  1079.                           usernameField.value, null,
  1080.                           usernameField.name, passwordField.name);
  1081.  
  1082.         // Look for a existing login and use its password.
  1083.         var match = null;
  1084.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  1085.  
  1086.         if (!logins.some(function(l) {
  1087.                                 match = l;
  1088.                                 return currentLogin.equalsIgnorePassword(l);
  1089.                         }))
  1090.         {
  1091.             this.log("Can't find a login for this autocomplete result.");
  1092.             return;
  1093.         }
  1094.  
  1095.         this.log("Found a matching login, filling in password.");
  1096.         passwordField.value = match.password;
  1097.     }
  1098. }; // end of LoginManager implementation
  1099.  
  1100.  
  1101.  
  1102.  
  1103. // nsIAutoCompleteResult implementation
  1104. function UserAutoCompleteResult (aSearchString, matchingLogins) {
  1105.     function loginSort(a,b) {
  1106.         var userA = a.username.toLowerCase();
  1107.         var userB = b.username.toLowerCase();
  1108.  
  1109.         if (userA < userB)
  1110.             return -1;
  1111.  
  1112.         if (userB > userA)
  1113.             return  1;
  1114.  
  1115.         return 0;
  1116.     };
  1117.  
  1118.     this.searchString = aSearchString;
  1119.     this.logins = matchingLogins.sort(loginSort);
  1120.     this.matchCount = matchingLogins.length;
  1121.  
  1122.     if (this.matchCount > 0) {
  1123.         this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
  1124.         this.defaultIndex = 0;
  1125.     }
  1126. }
  1127.  
  1128. UserAutoCompleteResult.prototype = {
  1129.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
  1130.                                             Ci.nsISupportsWeakReference]),
  1131.  
  1132.     // private
  1133.     logins : null,
  1134.  
  1135.     // Interfaces from idl...
  1136.     searchString : null,
  1137.     searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
  1138.     defaultIndex : -1,
  1139.     errorDescription : "",
  1140.     matchCount : 0,
  1141.  
  1142.     getValueAt : function (index) {
  1143.         if (index < 0 || index >= this.logins.length)
  1144.             throw "Index out of range.";
  1145.  
  1146.         return this.logins[index].username;
  1147.     },
  1148.  
  1149.     getCommentAt : function (index) {
  1150.         return "";
  1151.     },
  1152.  
  1153.     getStyleAt : function (index) {
  1154.         return "";
  1155.     },
  1156.  
  1157.     getImageAt : function (index) {
  1158.         return "";
  1159.     },
  1160.  
  1161.     removeValueAt : function (index, removeFromDB) {
  1162.         if (index < 0 || index >= this.logins.length)
  1163.             throw "Index out of range.";
  1164.  
  1165.         var [removedLogin] = this.logins.splice(index, 1);
  1166.  
  1167.         this.matchCount--;
  1168.         if (this.defaultIndex > this.logins.length)
  1169.             this.defaultIndex--;
  1170.  
  1171.         if (removeFromDB) {
  1172.             var pwmgr = Cc["@mozilla.org/login-manager;1"]
  1173.                             .getService(Ci.nsILoginManager);
  1174.             pwmgr.removeLogin(removedLogin);
  1175.         }
  1176.     },
  1177. };
  1178.  
  1179. var component = [LoginManager];
  1180. function NSGetModule (compMgr, fileSpec) {
  1181.     return XPCOMUtils.generateModule(component);
  1182. }
  1183.