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