home *** CD-ROM | disk | FTP | other *** search
/ PC World 2007 December / PCWorld_2007-12_cd.bin / audio-video / songbird / Songbird_0.3_windows-i686.exe / xulrunner / components / nsLoginManager.js < prev    next >
Text File  |  2007-09-26  |  42KB  |  1,191 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.             // XXX The C++ code took care to avoid reentrancy if a
  528.             // master-password dialog was triggered here, but since
  529.             // we're decrypting at load time that can't happen right now.
  530.             this.log("Creating new autocomplete search result.");
  531.  
  532.             var doc = aElement.ownerDocument;
  533.             var origin = this._getPasswordOrigin(doc.documentURI);
  534.             var actionOrigin = this._getActionOrigin(aElement.form);
  535.  
  536.             var logins = this.findLogins({}, origin, actionOrigin, null);
  537.             var matchingLogins = [];
  538.  
  539.             for (i = 0; i < logins.length; i++) {
  540.                 var username = logins[i].username.toLowerCase();
  541.                 if (aSearchString.length <= username.length &&
  542.                     aSearchString.toLowerCase() ==
  543.                         username.substr(0, aSearchString.length))
  544.                 {
  545.                     matchingLogins.push(logins[i]);
  546.                 }
  547.             }
  548.             this.log(matchingLogins.length + " autocomplete logins avail.");
  549.             result = new UserAutoCompleteResult(aSearchString, matchingLogins);
  550.         }
  551.  
  552.         return result;
  553.     },
  554.  
  555.  
  556.  
  557.  
  558.     /* ------- Internal methods / callbacks for document integration ------- */
  559.  
  560.  
  561.  
  562.  
  563.     /*
  564.      * _getPasswordFields
  565.      *
  566.      * Returns an array of password field elements for the specified form.
  567.      * If no pw fields are found, or if more than 3 are found, then null
  568.      * is returned.
  569.      *
  570.      * skipEmptyFields can be set to ignore password fields with no value.
  571.      */
  572.     _getPasswordFields : function (form, skipEmptyFields) {
  573.         // Locate the password fields in the form.
  574.         var pwFields = [];
  575.         for (var i = 0; i < form.elements.length; i++) {
  576.             if (form.elements[i].type != "password")
  577.                 continue;
  578.  
  579.             if (skipEmptyFields && !form.elements[i].value)
  580.                 continue;
  581.  
  582.             pwFields[pwFields.length] = {
  583.                                             index   : i,
  584.                                             element : form.elements[i]
  585.                                         };
  586.         }
  587.  
  588.         // If too few or too many fields, bail out.
  589.         if (pwFields.length == 0) {
  590.             this.log("(form ignored -- no password fields.)");
  591.             return null;
  592.         } else if (pwFields.length > 3) {
  593.             this.log("(form ignored -- too many password fields. [got " +
  594.                         pwFields.length + "])");
  595.             return null;
  596.         }
  597.  
  598.         return pwFields;
  599.     },
  600.  
  601.  
  602.     /*
  603.      * _getFormFields
  604.      *
  605.      * Returns the username and password fields found in the form.
  606.      * Can handle complex forms by trying to figure out what the
  607.      * relevant fields are.
  608.      *
  609.      * Returns: [usernameField, newPasswordField, oldPasswordField]
  610.      *
  611.      * usernameField may be null.
  612.      * newPasswordField will always be non-null.
  613.      * oldPasswordField may be null. If null, newPasswordField is just
  614.      * "theLoginField". If not null, the form is apparently a
  615.      * change-password field, with oldPasswordField containing the password
  616.      * that is being changed.
  617.      */
  618.     _getFormFields : function (form, isSubmission) {
  619.         var usernameField = null;
  620.  
  621.         // Locate the password field(s) in the form. Up to 3 supported.
  622.         // If there's no password field, there's nothing for us to do.
  623.         var pwFields = this._getPasswordFields(form, isSubmission);
  624.         if (!pwFields)
  625.             return [null, null, null];
  626.  
  627.  
  628.         // Locate the username field in the form by searching backwards
  629.         // from the first passwordfield, assume the first text field is the
  630.         // username. We might not find a username field if the user is
  631.         // already logged in to the site. 
  632.         for (var i = pwFields[0].index - 1; i >= 0; i--) {
  633.             if (form.elements[i].type == "text") {
  634.                 usernameField = form.elements[i];
  635.                 break;
  636.             }
  637.         }
  638.  
  639.         if (!usernameField)
  640.             this.log("(form -- no username field found)");
  641.  
  642.  
  643.         // If we're not submitting a form (it's a page load), there are no
  644.         // password field values for us to use for identifying fields. So,
  645.         // just assume the first password field is the one to be filled in.
  646.         if (!isSubmission || pwFields.length == 1)
  647.             return [usernameField, pwFields[0].element, null];
  648.  
  649.  
  650.         // Try to figure out WTF is in the form based on the password values.
  651.         var oldPasswordField, newPasswordField;
  652.         var pw1 = pwFields[0].element.value;
  653.         var pw2 = pwFields[1].element.value;
  654.         var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
  655.  
  656.         if (pwFields.length == 3) {
  657.             // Look for two identical passwords, that's the new password
  658.  
  659.             if (pw1 == pw2 && pw2 == pw3) {
  660.                 // All 3 passwords the same? Weird! Treat as if 1 pw field.
  661.                 newPasswordField = pwFields[0].element;
  662.                 oldPasswordField = null;
  663.             } else if (pw1 == pw2) {
  664.                 newPasswordField = pwFields[0].element;
  665.                 oldPasswordField = pwFields[2].element;
  666.             } else if (pw2 == pw3) {
  667.                 oldPasswordField = pwFields[0].element;
  668.                 newPasswordField = pwFields[2].element;
  669.             } else  if (pw1 == pw3) {
  670.                 // A bit odd, but could make sense with the right page layout.
  671.                 newPasswordField = pwFields[0].element;
  672.                 oldPasswordField = pwFields[1].element;
  673.             } else {
  674.                 // We can't tell which of the 3 passwords should be saved.
  675.                 this.log("(form ignored -- all 3 pw fields differ)");
  676.                 return [null, null, null];
  677.             }
  678.         } else { // pwFields.length == 2
  679.             if (pw1 == pw2) {
  680.                 // Treat as if 1 pw field
  681.                 newPasswordField = pwFields[0].element;
  682.                 oldPasswordField = null;
  683.             } else {
  684.                 // Just assume that the 2nd password is the new password
  685.                 oldPasswordField = pwFields[0].element;
  686.                 newPasswordField = pwFields[1].element;
  687.             }
  688.         }
  689.  
  690.         return [usernameField, newPasswordField, oldPasswordField];
  691.     },
  692.  
  693.  
  694.     /*
  695.      * _onFormSubmit
  696.      *
  697.      * Called by the our observer when notified of a form submission.
  698.      * [Note that this happens before any DOM onsubmit handlers are invoked.]
  699.      * Looks for a password change in the submitted form, so we can update
  700.      * our stored password.
  701.      *
  702.      * XXX update actionURL of existing login, even if pw not being changed?
  703.      */
  704.     _onFormSubmit : function (form) {
  705.  
  706.         // local helper function
  707.         function autocompleteDisabled(element) {
  708.             if (element && element.hasAttribute("autocomplete") &&
  709.                 element.getAttribute("autocomplete").toLowerCase() == "off")
  710.                 return true;
  711.  
  712.            return false;
  713.         };
  714.  
  715.         // local helper function
  716.         function getPrompter(aWindow) {
  717.             var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
  718.                             createInstance(Ci.nsILoginManagerPrompter);
  719.             prompterSvc.init(aWindow);
  720.             return prompterSvc;
  721.         }
  722.  
  723.         var doc = form.ownerDocument;
  724.         var win = doc.defaultView;
  725.  
  726.         // If password saving is disabled (globally or for host), bail out now.
  727.         if (!this._remember)
  728.             return;
  729.  
  730.         var hostname      = this._getPasswordOrigin(doc.documentURI);
  731.         var formSubmitURL = this._getActionOrigin(form)
  732.         if (!this.getLoginSavingEnabled(hostname)) {
  733.             this.log("(form submission ignored -- saving is " +
  734.                      "disabled for: " + hostname + ")");
  735.             return;
  736.         }
  737.  
  738.  
  739.         // Get the appropriate fields from the form.
  740.         var [usernameField, newPasswordField, oldPasswordField] =
  741.             this._getFormFields(form, true);
  742.  
  743.         // Need at least 1 valid password field to do anything.
  744.         if (newPasswordField == null)
  745.                 return;
  746.  
  747.         // Check for autocomplete=off attribute. We don't use it to prevent
  748.         // autofilling (for existing logins), but won't save logins when it's
  749.         // present.
  750.         if (autocompleteDisabled(form) ||
  751.             autocompleteDisabled(usernameField) ||
  752.             autocompleteDisabled(newPasswordField) ||
  753.             autocompleteDisabled(oldPasswordField)) {
  754.                 this.log("(form submission ignored -- autocomplete=off found)");
  755.                 return;
  756.         }
  757.  
  758.  
  759.         var formLogin = new this._nsLoginInfo();
  760.         formLogin.init(hostname, formSubmitURL, null,
  761.                     (usernameField ? usernameField.value : ""),
  762.                     newPasswordField.value,
  763.                     (usernameField ? usernameField.name  : ""),
  764.                     newPasswordField.name);
  765.  
  766.         // If we didn't find a username field, but seem to be changing a
  767.         // password, allow the user to select from a list of applicable
  768.         // logins to update the password for.
  769.         if (!usernameField && oldPasswordField) {
  770.  
  771.             var logins = this.findLogins({}, hostname, formSubmitURL, null);
  772.  
  773.             // XXX we could be smarter here: look for a login matching the
  774.             // old password value. If there's only one, update it. If there's
  775.             // more than one we could filter the list (but, edge case: the
  776.             // login for the pwchange is in pwmgr, but with an outdated
  777.             // password. and the user has another login, with the same
  778.             // password as the form login's old password.) ugh.
  779.             // XXX if you're changing a password, and there's no username
  780.             // in the form, then you can't add the login. Will need to change
  781.             // prompting to allow this.
  782.  
  783.             if (logins.length == 0) {
  784.                 this.log("(no logins for this host -- pwchange ignored)");
  785.                 return;
  786.             }
  787.  
  788.             var prompter = getPrompter(win);
  789.  
  790.             if (logins.length == 1) {
  791.                 var oldLogin = logins[0];
  792.                 formLogin.username      = oldLogin.username;
  793.                 formLogin.usernameField = oldLogin.usernameField;
  794.  
  795.                 prompter.promptToChangePassword(oldLogin, formLogin);
  796.             } else {
  797.                 prompter.promptToChangePasswordWithUsernames(
  798.                                     logins, logins.length, formLogin);
  799.             }
  800.  
  801.             return;
  802.         }
  803.  
  804.  
  805.         // Look for an existing login that matches the form login.
  806.         var existingLogin = null;
  807.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  808.  
  809.         for (var i = 0; i < logins.length; i++) {
  810.             var same, login = logins[i];
  811.  
  812.             // If one login has a username but the other doesn't, ignore
  813.             // the username when comparing and only match if they have the
  814.             // same password. Otherwise, compare the logins and match even
  815.             // if the passwords differ.
  816.             if (!login.username && formLogin.username) {
  817.                 var restoreMe = formLogin.username;
  818.                 formLogin.username = ""; 
  819.                 same = formLogin.equals(login);
  820.                 formLogin.username = restoreMe;
  821.             } else if (!formLogin.username && login.username) {
  822.                 formLogin.username = login.username;
  823.                 same = formLogin.equals(login);
  824.                 formLogin.username = ""; // we know it's always blank.
  825.             } else {
  826.                 same = formLogin.equalsIgnorePassword(login);
  827.             }
  828.  
  829.             if (same) {
  830.                 existingLogin = login;
  831.                 break;
  832.             }
  833.         }
  834.  
  835.         if (existingLogin) {
  836.             this.log("Found an existing login matching this form submission");
  837.  
  838.             /*
  839.              * Change password if needed.
  840.              *
  841.              * If the login has a username, change the password w/o prompting
  842.              * (because we can be fairly sure there's only one password
  843.              * associated with the username). But for logins without a
  844.              * username, ask the user... Some sites use a password-only "login"
  845.              * in different contexts (enter your PIN, answer a security
  846.              * question, etc), and without a username we can't be sure if
  847.              * modifying an existing login is the right thing to do.
  848.              */
  849.             if (existingLogin.password != formLogin.password) {
  850.                 if (formLogin.username) {
  851.                     this.log("...Updating password for existing login.");
  852.                     this.modifyLogin(existingLogin, formLogin);
  853.                 } else {
  854.                     this.log("...passwords differ, prompting to change.");
  855.                     prompter = getPrompter(win);
  856.                     prompter.promptToChangePassword(existingLogin, formLogin);
  857.                 }
  858.             }
  859.  
  860.             return;
  861.         }
  862.  
  863.  
  864.         // Prompt user to save login (via dialog or notification bar)
  865.         prompter = getPrompter(win);
  866.         prompter.promptToSavePassword(formLogin);
  867.     },
  868.  
  869.  
  870.     /*
  871.      * _getPasswordOrigin
  872.      *
  873.      * Get the parts of the URL we want for identification.
  874.      */
  875.     _getPasswordOrigin : function (uriString) {
  876.         var realm = "";
  877.         try {
  878.             var uri = this._ioService.newURI(uriString, null, null);
  879.  
  880.             realm += uri.scheme;
  881.             realm += "://";
  882.             realm += uri.hostPort;
  883.         } catch (e) {
  884.             // bug 159484 - disallow url types that don't support a hostPort.
  885.             // (set null to cause throw in the JS above)
  886.             realm = null;
  887.         }
  888.  
  889.         return realm;
  890.     },
  891.  
  892.     _getActionOrigin : function (form) {
  893.         var uriString = form.action;
  894.  
  895.         // A blank or mission action submits to where it came from.
  896.         if (uriString == "")
  897.             uriString = form.baseURI; // ala bug 297761
  898.  
  899.         return this._getPasswordOrigin(uriString);
  900.     },
  901.  
  902.  
  903.     /*
  904.      * _fillDocument
  905.      *
  906.      * Called when a page has loaded. For each form in the document,
  907.      * we check to see if it can be filled with a stored login.
  908.      */
  909.     _fillDocument : function (doc) {
  910.         var forms = doc.forms;
  911.         if (!forms || forms.length == 0)
  912.             return;
  913.  
  914.         var formOrigin = this._getPasswordOrigin(doc.documentURI);
  915.  
  916.         // If there are no logins for this site, bail out now.
  917.         if (!this.countLogins(formOrigin, "", null))
  918.             return;
  919.  
  920.         this.log("fillDocument processing " + forms.length +
  921.                  " forms on " + doc.documentURI);
  922.  
  923.         var autofillForm = this._prefBranch.getBoolPref("autofillForms");
  924.         var previousActionOrigin = null;
  925.  
  926.         for (var i = 0; i < forms.length; i++) {
  927.             var form = forms[i];
  928.  
  929.             // Heuristically determine what the user/pass fields are
  930.             // We do this before checking to see if logins are stored,
  931.             // so that the user isn't prompted for a master password
  932.             // without need.
  933.             var [usernameField, passwordField, ignored] =
  934.                 this._getFormFields(form, false);
  935.  
  936.             // Need a valid password field to do anything.
  937.             if (passwordField == null)
  938.                 continue;
  939.  
  940.  
  941.             // Only the actionOrigin might be changing, so if it's the same
  942.             // as the last form on the page we can reuse the same logins.
  943.             var actionOrigin = this._getActionOrigin(form);
  944.             if (actionOrigin != previousActionOrigin) {
  945.                 var foundLogins =
  946.                     this.findLogins({}, formOrigin, actionOrigin, null);
  947.  
  948.                 this.log("form[" + i + "]: got " +
  949.                          foundLogins.length + " logins.");
  950.  
  951.                 previousActionOrigin = actionOrigin;
  952.             } else {
  953.                 this.log("form[" + i + "]: using logins from last form.");
  954.             }
  955.  
  956.  
  957.             // Discard logins which have username/password values that don't
  958.             // fit into the fields (as specified by the maxlength attribute).
  959.             // The user couldn't enter these values anyway, and it helps
  960.             // with sites that have an extra PIN to be entered (bug 391514)
  961.             var maxUsernameLen = Number.MAX_VALUE;
  962.             var maxPasswordLen = Number.MAX_VALUE;
  963.  
  964.             // If attribute wasn't set, default is -1.
  965.             if (usernameField && usernameField.maxLength >= 0)
  966.                 maxUsernameLen = usernameField.maxLength;
  967.             if (passwordField.maxLength >= 0)
  968.                 maxPasswordLen = passwordField.maxLength;
  969.  
  970.             logins = foundLogins.filter(function (l) {
  971.                     var fit = (l.username.length <= maxUsernameLen &&
  972.                                l.password.length <= maxPasswordLen);
  973.                     if (!fit)
  974.                         this.log("Ignored " + l.username + " login: won't fit");
  975.  
  976.                     return fit;
  977.                 }, this);
  978.  
  979.  
  980.             // Nothing to do if we have no matching logins available.
  981.             if (logins.length == 0)
  982.                 continue;
  983.  
  984.  
  985.             // Attach autocomplete stuff to the username field, if we have
  986.             // one. This is normally used to select from multiple accounts,
  987.             // but even with one account we should refill if the user edits.
  988.             // XXX should be able to pass in |logins| to init attachment
  989.             if (usernameField)
  990.                 this._attachToInput(usernameField);
  991.  
  992.             if (autofillForm) {
  993.  
  994.                 if (usernameField && usernameField.value) {
  995.                     // If username was specified in the form, only fill in the
  996.                     // password if we find a matching login.
  997.  
  998.                     var username = usernameField.value;
  999.  
  1000.                     var matchingLogin;
  1001.                     var found = logins.some(function(l) {
  1002.                                                 matchingLogin = l;
  1003.                                                 return (l.username == username);
  1004.                                             });
  1005.                     if (found)
  1006.                         passwordField.value = matchingLogin.password;
  1007.  
  1008.                 } else if (usernameField && logins.length == 2) {
  1009.                     // Special case, for sites which have a normal user+pass
  1010.                     // login *and* a password-only login (eg, a PIN)...
  1011.                     // When we have a username field and 1 of 2 available
  1012.                     // logins is password-only, go ahead and prefill the
  1013.                     // one with a username.
  1014.                     if (!logins[0].username && logins[1].username) {
  1015.                         usernameField.value = logins[1].username;
  1016.                         passwordField.value = logins[1].password;
  1017.                     } else if (!logins[1].username && logins[0].username) {
  1018.                         usernameField.value = logins[0].username;
  1019.                         passwordField.value = logins[0].password;
  1020.                     }
  1021.                 } else if (logins.length == 1) {
  1022.                     if (usernameField)
  1023.                         usernameField.value = logins[0].username;
  1024.                     passwordField.value = logins[0].password;
  1025.                 }
  1026.             }
  1027.         } // foreach form
  1028.     },
  1029.  
  1030.  
  1031.     /*
  1032.      * _attachToInput
  1033.      *
  1034.      * Hooks up autocomplete support to a username field, to allow
  1035.      * a user editing the field to select an existing login and have
  1036.      * the password field filled in.
  1037.      */
  1038.     _attachToInput : function (element) {
  1039.         this.log("attaching autocomplete stuff");
  1040.         element.addEventListener("blur",
  1041.                                 this._domEventListener, false);
  1042.         element.addEventListener("DOMAutoComplete",
  1043.                                 this._domEventListener, false);
  1044.         this._formFillService.markAsLoginManagerField(element);
  1045.     },
  1046.  
  1047.  
  1048.     /*
  1049.      * _fillPassword
  1050.      *
  1051.      * The user has autocompleted a username field, so fill in the password.
  1052.      */
  1053.     _fillPassword : function (usernameField) {
  1054.         this.log("fillPassword autocomplete username: " + usernameField.value);
  1055.  
  1056.         var form = usernameField.form;
  1057.         var doc = form.ownerDocument;
  1058.  
  1059.         var hostname = this._getPasswordOrigin(doc.documentURI);
  1060.         var formSubmitURL = this._getActionOrigin(form)
  1061.  
  1062.         // Find the password field. We should always have at least one,
  1063.         // or else something has gone rather wrong.
  1064.         var pwFields = this._getPasswordFields(form, false);
  1065.         if (!pwFields) {
  1066.             const err = "No password field for autocomplete password fill.";
  1067.  
  1068.             // We want to know about this even if debugging is disabled.
  1069.             if (!this._debug)
  1070.                 dump(err);
  1071.             else
  1072.                 this.log(err);
  1073.  
  1074.             return;
  1075.         }
  1076.  
  1077.         // XXX: we could do better on forms with 2 or 3 password fields.
  1078.         var passwordField = pwFields[0].element;
  1079.  
  1080.         // XXX this would really be cleaner if we could get at the
  1081.         // AutoCompleteResult, which has the actual nsILoginInfo for the
  1082.         // username selected.
  1083.  
  1084.         // Temporary LoginInfo with the info we know.
  1085.         var currentLogin = new this._nsLoginInfo();
  1086.         currentLogin.init(hostname, formSubmitURL, null,
  1087.                           usernameField.value, null,
  1088.                           usernameField.name, passwordField.name);
  1089.  
  1090.         // Look for a existing login and use its password.
  1091.         var match = null;
  1092.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  1093.  
  1094.         if (!logins.some(function(l) {
  1095.                                 match = l;
  1096.                                 return currentLogin.equalsIgnorePassword(l);
  1097.                         }))
  1098.         {
  1099.             this.log("Can't find a login for this autocomplete result.");
  1100.             return;
  1101.         }
  1102.  
  1103.         this.log("Found a matching login, filling in password.");
  1104.         passwordField.value = match.password;
  1105.     }
  1106. }; // end of LoginManager implementation
  1107.  
  1108.  
  1109.  
  1110.  
  1111. // nsIAutoCompleteResult implementation
  1112. function UserAutoCompleteResult (aSearchString, matchingLogins) {
  1113.     function loginSort(a,b) {
  1114.         var userA = a.username.toLowerCase();
  1115.         var userB = b.username.toLowerCase();
  1116.  
  1117.         if (userA < userB)
  1118.             return -1;
  1119.  
  1120.         if (userB > userA)
  1121.             return  1;
  1122.  
  1123.         return 0;
  1124.     };
  1125.  
  1126.     this.searchString = aSearchString;
  1127.     this.logins = matchingLogins.sort(loginSort);
  1128.     this.matchCount = matchingLogins.length;
  1129.  
  1130.     if (this.matchCount > 0) {
  1131.         this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
  1132.         this.defaultIndex = 0;
  1133.     }
  1134. }
  1135.  
  1136. UserAutoCompleteResult.prototype = {
  1137.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
  1138.                                             Ci.nsISupportsWeakReference]),
  1139.  
  1140.     // private
  1141.     logins : null,
  1142.  
  1143.     // Interfaces from idl...
  1144.     searchString : null,
  1145.     searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
  1146.     defaultIndex : -1,
  1147.     errorDescription : "",
  1148.     matchCount : 0,
  1149.  
  1150.     getValueAt : function (index) {
  1151.         if (index < 0 || index >= this.logins.length)
  1152.             throw "Index out of range.";
  1153.  
  1154.         return this.logins[index].username;
  1155.     },
  1156.  
  1157.     getCommentAt : function (index) {
  1158.         return "";
  1159.     },
  1160.  
  1161.     getStyleAt : function (index) {
  1162.         return "";
  1163.     },
  1164.  
  1165.     getImageAt : function (index) {
  1166.         return "";
  1167.     },
  1168.  
  1169.     removeValueAt : function (index, removeFromDB) {
  1170.         if (index < 0 || index >= this.logins.length)
  1171.             throw "Index out of range.";
  1172.  
  1173.         var [removedLogin] = this.logins.splice(index, 1);
  1174.  
  1175.         this.matchCount--;
  1176.         if (this.defaultIndex > this.logins.length)
  1177.             this.defaultIndex--;
  1178.  
  1179.         if (removeFromDB) {
  1180.             var pwmgr = Cc["@mozilla.org/login-manager;1"]
  1181.                             .getService(Ci.nsILoginManager);
  1182.             pwmgr.removeLogin(removedLogin);
  1183.         }
  1184.     },
  1185. };
  1186.  
  1187. var component = [LoginManager];
  1188. function NSGetModule (compMgr, fileSpec) {
  1189.     return XPCOMUtils.generateModule(component);
  1190. }
  1191.