home *** CD-ROM | disk | FTP | other *** search
/ PC Advisor 2010 April / PCA177.iso / ESSENTIALS / Firefox Setup.exe / nonlocalized / components / nsLoginManagerPrompter.js < prev    next >
Encoding:
Text File  |  2009-07-15  |  43.6 KB  |  1,230 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.  *  Ehsan Akhgari <ehsan.akhgari@gmail.com>
  23.  *
  24.  * Alternatively, the contents of this file may be used under the terms of
  25.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  26.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27.  * in which case the provisions of the GPL or the LGPL are applicable instead
  28.  * of those above. If you wish to allow use of your version of this file only
  29.  * under the terms of either the GPL or the LGPL, and not to allow others to
  30.  * use your version of this file under the terms of the MPL, indicate your
  31.  * decision by deleting the provisions above and replace them with the notice
  32.  * and other provisions required by the GPL or the LGPL. If you do not delete
  33.  * the provisions above, a recipient may use your version of this file under
  34.  * the terms of any one of the MPL, the GPL or the LGPL.
  35.  *
  36.  * ***** END LICENSE BLOCK ***** */
  37.  
  38.  
  39. const Cc = Components.classes;
  40. const Ci = Components.interfaces;
  41. const Cr = Components.results;
  42.  
  43. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  44.  
  45. /*
  46.  * LoginManagerPromptFactory
  47.  *
  48.  * Implements nsIPromptFactory
  49.  *
  50.  * Invoked by NS_NewAuthPrompter2()
  51.  * [embedding/components/windowwatcher/src/nsPrompt.cpp]
  52.  */
  53. function LoginManagerPromptFactory() {}
  54.  
  55. LoginManagerPromptFactory.prototype = {
  56.  
  57.     classDescription : "LoginManagerPromptFactory",
  58.     contractID : "@mozilla.org/passwordmanager/authpromptfactory;1",
  59.     classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
  60.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
  61.  
  62.     getPrompt : function (aWindow, aIID) {
  63.         var prompt = new LoginManagerPrompter().QueryInterface(aIID);
  64.         prompt.init(aWindow);
  65.         return prompt;
  66.     }
  67. }; // end of LoginManagerPromptFactory implementation
  68.  
  69.  
  70.  
  71.  
  72. /* ==================== LoginManagerPrompter ==================== */
  73.  
  74.  
  75.  
  76.  
  77. /*
  78.  * LoginManagerPrompter
  79.  *
  80.  * Implements interfaces for prompting the user to enter/save/change auth info.
  81.  *
  82.  * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
  83.  *
  84.  * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
  85.  * (eg HTTP Authenticate, FTP login).
  86.  *
  87.  * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
  88.  * found in HTML forms.
  89.  */
  90. function LoginManagerPrompter() {}
  91.  
  92. LoginManagerPrompter.prototype = {
  93.  
  94.     classDescription : "LoginManagerPrompter",
  95.     contractID : "@mozilla.org/login-manager/prompter;1",
  96.     classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
  97.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
  98.                                             Ci.nsIAuthPrompt2,
  99.                                             Ci.nsILoginManagerPrompter]),
  100.  
  101.     _window        : null,
  102.     _debug         : false, // mirrors signon.debug
  103.  
  104.     __pwmgr : null, // Password Manager service
  105.     get _pwmgr() {
  106.         if (!this.__pwmgr)
  107.             this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
  108.                            getService(Ci.nsILoginManager);
  109.         return this.__pwmgr;
  110.     },
  111.  
  112.     __logService : null, // Console logging service, used for debugging.
  113.     get _logService() {
  114.         if (!this.__logService)
  115.             this.__logService = Cc["@mozilla.org/consoleservice;1"].
  116.                                 getService(Ci.nsIConsoleService);
  117.         return this.__logService;
  118.     },
  119.  
  120.     __promptService : null, // Prompt service for user interaction
  121.     get _promptService() {
  122.         if (!this.__promptService)
  123.             this.__promptService =
  124.                 Cc["@mozilla.org/embedcomp/prompt-service;1"].
  125.                 getService(Ci.nsIPromptService2);
  126.         return this.__promptService;
  127.     },
  128.  
  129.  
  130.     __strBundle : null, // String bundle for L10N
  131.     get _strBundle() {
  132.         if (!this.__strBundle) {
  133.             var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
  134.                              getService(Ci.nsIStringBundleService);
  135.             this.__strBundle = bunService.createBundle(
  136.                         "chrome://passwordmgr/locale/passwordmgr.properties");
  137.             if (!this.__strBundle)
  138.                 throw "String bundle for Login Manager not present!";
  139.         }
  140.  
  141.         return this.__strBundle;
  142.     },
  143.  
  144.  
  145.     __brandBundle : null, // String bundle for L10N
  146.     get _brandBundle() {
  147.         if (!this.__brandBundle) {
  148.             var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
  149.                              getService(Ci.nsIStringBundleService);
  150.             this.__brandBundle = bunService.createBundle(
  151.                         "chrome://branding/locale/brand.properties");
  152.             if (!this.__brandBundle)
  153.                 throw "Branding string bundle not present!";
  154.         }
  155.  
  156.         return this.__brandBundle;
  157.     },
  158.  
  159.  
  160.     __ioService: null, // IO service for string -> nsIURI conversion
  161.     get _ioService() {
  162.         if (!this.__ioService)
  163.             this.__ioService = Cc["@mozilla.org/network/io-service;1"].
  164.                                getService(Ci.nsIIOService);
  165.         return this.__ioService;
  166.     },
  167.  
  168.  
  169.     __ellipsis : null,
  170.     get _ellipsis() {
  171.         if (!this.__ellipsis) {
  172.             this.__ellipsis = "\u2026";
  173.             try {
  174.                 var prefSvc = Cc["@mozilla.org/preferences-service;1"].
  175.                               getService(Ci.nsIPrefBranch);
  176.                 this.__ellipsis = prefSvc.getComplexValue("intl.ellipsis",
  177.                                       Ci.nsIPrefLocalizedString).data;
  178.             } catch (e) { }
  179.         }
  180.         return this.__ellipsis;
  181.     },
  182.  
  183.  
  184.     // Whether we are in private browsing mode
  185.     get _inPrivateBrowsing() {
  186.       // The Private Browsing service might not be available.
  187.       try {
  188.         var pbs = Cc["@mozilla.org/privatebrowsing;1"].
  189.                   getService(Ci.nsIPrivateBrowsingService);
  190.         return pbs.privateBrowsingEnabled;
  191.       } catch (e) {
  192.         return false;
  193.       }
  194.     },
  195.  
  196.  
  197.     /*
  198.      * log
  199.      *
  200.      * Internal function for logging debug messages to the Error Console window.
  201.      */
  202.     log : function (message) {
  203.         if (!this._debug)
  204.             return;
  205.  
  206.         dump("Pwmgr Prompter: " + message + "\n");
  207.         this._logService.logStringMessage("Pwmgr Prompter: " + message);
  208.     },
  209.  
  210.  
  211.  
  212.  
  213.     /* ---------- nsIAuthPrompt prompts ---------- */
  214.  
  215.  
  216.     /*
  217.      * prompt
  218.      *
  219.      * Wrapper around the prompt service prompt. Saving random fields here
  220.      * doesn't really make sense and therefore isn't implemented.
  221.      */
  222.     prompt : function (aDialogTitle, aText, aPasswordRealm,
  223.                        aSavePassword, aDefaultText, aResult) {
  224.         if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
  225.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  226.  
  227.         this.log("===== prompt() called =====");
  228.  
  229.         if (aDefaultText) {
  230.             aResult.value = aDefaultText;
  231.         }
  232.  
  233.         return this._promptService.prompt(this._window,
  234.                aDialogTitle, aText, aResult, null, {});
  235.     },
  236.  
  237.  
  238.     /*
  239.      * promptUsernameAndPassword
  240.      *
  241.      * Looks up a username and password in the database. Will prompt the user
  242.      * with a dialog, even if a username and password are found.
  243.      */
  244.     promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
  245.                                          aSavePassword, aUsername, aPassword) {
  246.         this.log("===== promptUsernameAndPassword() called =====");
  247.  
  248.         if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
  249.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  250.  
  251.         var selectedLogin = null;
  252.         var checkBox = { value : false };
  253.         var checkBoxLabel = null;
  254.         var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
  255.  
  256.         // If hostname is null, we can't save this login.
  257.         if (hostname) {
  258.             var canRememberLogin;
  259.             if (this._inPrivateBrowsing)
  260.                 canRememberLogin = false;
  261.             else
  262.                 canRememberLogin = (aSavePassword ==
  263.                                     Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
  264.                                    this._pwmgr.getLoginSavingEnabled(hostname);
  265.  
  266.             // if checkBoxLabel is null, the checkbox won't be shown at all.
  267.             if (canRememberLogin)
  268.                 checkBoxLabel = this._getLocalizedString("rememberPassword");
  269.  
  270.             // Look for existing logins.
  271.             var foundLogins = this._pwmgr.findLogins({}, hostname, null,
  272.                                                      realm);
  273.  
  274.             // XXX Like the original code, we can't deal with multiple
  275.             // account selection. (bug 227632)
  276.             if (foundLogins.length > 0) {
  277.                 selectedLogin = foundLogins[0];
  278.  
  279.                 // If the caller provided a username, try to use it. If they
  280.                 // provided only a password, this will try to find a password-only
  281.                 // login (or return null if none exists).
  282.                 if (aUsername.value)
  283.                     selectedLogin = this._repickSelectedLogin(foundLogins,
  284.                                                               aUsername.value);
  285.  
  286.                 if (selectedLogin) {
  287.                     checkBox.value = true;
  288.                     aUsername.value = selectedLogin.username;
  289.                     // If the caller provided a password, prefer it.
  290.                     if (!aPassword.value)
  291.                         aPassword.value = selectedLogin.password;
  292.                 }
  293.             }
  294.         }
  295.  
  296.         var ok = this._promptService.promptUsernameAndPassword(this._window,
  297.                     aDialogTitle, aText, aUsername, aPassword,
  298.                     checkBoxLabel, checkBox);
  299.  
  300.         if (!ok || !checkBox.value || !hostname)
  301.             return ok;
  302.  
  303.         if (!aPassword.value) {
  304.             this.log("No password entered, so won't offer to save.");
  305.             return ok;
  306.         }
  307.  
  308.         var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
  309.                        createInstance(Ci.nsILoginInfo);
  310.         newLogin.init(hostname, null, realm, aUsername.value, aPassword.value,
  311.                       "", "");
  312.  
  313.         // XXX We can't prompt with multiple logins yet (bug 227632), so
  314.         // the entered login might correspond to an existing login
  315.         // other than the one we originally selected.
  316.         selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
  317.  
  318.         // If we didn't find an existing login, or if the username
  319.         // changed, save as a new login.
  320.         if (!selectedLogin) {
  321.             // add as new
  322.             this.log("New login seen for " + realm);
  323.             this._pwmgr.addLogin(newLogin);
  324.         } else if (aPassword.value != selectedLogin.password) {
  325.             // update password
  326.             this.log("Updating password for  " + realm);
  327.             this._pwmgr.modifyLogin(selectedLogin, newLogin);
  328.         } else {
  329.             this.log("Login unchanged, no further action needed.");
  330.         }
  331.  
  332.         return ok;
  333.     },
  334.  
  335.  
  336.     /*
  337.      * promptPassword
  338.      *
  339.      * If a password is found in the database for the password realm, it is
  340.      * returned straight away without displaying a dialog.
  341.      *
  342.      * If a password is not found in the database, the user will be prompted
  343.      * with a dialog with a text field and ok/cancel buttons. If the user
  344.      * allows it, then the password will be saved in the database.
  345.      */
  346.     promptPassword : function (aDialogTitle, aText, aPasswordRealm,
  347.                                aSavePassword, aPassword) {
  348.         this.log("===== promptPassword called() =====");
  349.  
  350.         if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
  351.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  352.  
  353.         var checkBox = { value : false };
  354.         var checkBoxLabel = null;
  355.         var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
  356.  
  357.         username = decodeURIComponent(username);
  358.  
  359.         // If hostname is null, we can't save this login.
  360.         if (hostname && !this._inPrivateBrowsing) {
  361.           var canRememberLogin = (aSavePassword ==
  362.                                   Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
  363.                                  this._pwmgr.getLoginSavingEnabled(hostname);
  364.   
  365.           // if checkBoxLabel is null, the checkbox won't be shown at all.
  366.           if (canRememberLogin)
  367.               checkBoxLabel = this._getLocalizedString("rememberPassword");
  368.   
  369.           if (!aPassword.value) {
  370.               // Look for existing logins.
  371.               var foundLogins = this._pwmgr.findLogins({}, hostname, null,
  372.                                                        realm);
  373.   
  374.               // XXX Like the original code, we can't deal with multiple
  375.               // account selection (bug 227632). We can deal with finding the
  376.               // account based on the supplied username - but in this case we'll
  377.               // just return the first match.
  378.               for (var i = 0; i < foundLogins.length; ++i) {
  379.                   if (foundLogins[i].username == username) {
  380.                       aPassword.value = foundLogins[i].password;
  381.                       // wallet returned straight away, so this mimics that code
  382.                       return true;
  383.                   }
  384.               }
  385.           }
  386.         }
  387.  
  388.         var ok = this._promptService.promptPassword(this._window, aDialogTitle,
  389.                                                     aText, aPassword,
  390.                                                     checkBoxLabel, checkBox);
  391.  
  392.         if (ok && checkBox.value && hostname && aPassword.value) {
  393.             var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
  394.                            createInstance(Ci.nsILoginInfo);
  395.             newLogin.init(hostname, null, realm, username,
  396.                           aPassword.value, "", "");
  397.  
  398.             this.log("New login seen for " + realm);
  399.  
  400.             this._pwmgr.addLogin(newLogin);
  401.         }
  402.  
  403.         return ok;
  404.     },
  405.  
  406.     /* ---------- nsIAuthPrompt helpers ---------- */
  407.  
  408.  
  409.     /**
  410.      * Given aRealmString, such as "http://user@example.com/foo", returns an
  411.      * array of:
  412.      *   - the formatted hostname
  413.      *   - the realm (hostname + path)
  414.      *   - the username, if present
  415.      *
  416.      * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
  417.      * channels, e.g. "example.com:80 (httprealm)", null is returned for all
  418.      * arguments to let callers know the login can't be saved because we don't
  419.      * know whether it's http or https.
  420.      */
  421.     _getRealmInfo : function (aRealmString) {
  422.         var httpRealm = /^.+ \(.+\)$/;
  423.         if (httpRealm.test(aRealmString))
  424.             return [null, null, null];
  425.  
  426.         var uri = this._ioService.newURI(aRealmString, null, null);
  427.         var pathname = "";
  428.  
  429.         if (uri.path != "/")
  430.             pathname = uri.path;
  431.  
  432.         var formattedHostname = this._getFormattedHostname(uri);
  433.  
  434.         return [formattedHostname, formattedHostname + pathname, uri.username];
  435.     },
  436.  
  437.     /* ---------- nsIAuthPrompt2 prompts ---------- */
  438.  
  439.  
  440.  
  441.  
  442.     /*
  443.      * promptAuth
  444.      *
  445.      * Implementation of nsIAuthPrompt2.
  446.      *
  447.      * nsIChannel aChannel
  448.      * int        aLevel
  449.      * nsIAuthInformation aAuthInfo
  450.      */
  451.     promptAuth : function (aChannel, aLevel, aAuthInfo) {
  452.         var selectedLogin = null;
  453.         var checkbox = { value : false };
  454.         var checkboxLabel = null;
  455.         var epicfail = false;
  456.  
  457.         try {
  458.  
  459.             this.log("===== promptAuth called =====");
  460.  
  461.             // If the user submits a login but it fails, we need to remove the
  462.             // notification bar that was displayed. Conveniently, the user will
  463.             // be prompted for authentication again, which brings us here.
  464.             var notifyBox = this._getNotifyBox();
  465.             if (notifyBox)
  466.                 this._removeLoginNotifications(notifyBox);
  467.  
  468.             var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
  469.  
  470.  
  471.             // Looks for existing logins to prefill the prompt with.
  472.             var foundLogins = this._pwmgr.findLogins({},
  473.                                         hostname, null, httpRealm);
  474.             this.log("found " + foundLogins.length + " matching logins.");
  475.  
  476.             // XXX Can't select from multiple accounts yet. (bug 227632)
  477.             if (foundLogins.length > 0) {
  478.                 selectedLogin = foundLogins[0];
  479.                 this._SetAuthInfo(aAuthInfo, selectedLogin.username,
  480.                                              selectedLogin.password);
  481.                 checkbox.value = true;
  482.             }
  483.  
  484.             var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
  485.             if (this._inPrivateBrowsing)
  486.               canRememberLogin = false;
  487.         
  488.             // if checkboxLabel is null, the checkbox won't be shown at all.
  489.             if (canRememberLogin && !notifyBox)
  490.                 checkboxLabel = this._getLocalizedString("rememberPassword");
  491.         } catch (e) {
  492.             // Ignore any errors and display the prompt anyway.
  493.             epicfail = true;
  494.             Components.utils.reportError("LoginManagerPrompter: " +
  495.                 "Epic fail in promptAuth: " + e + "\n");
  496.         }
  497.  
  498.         var ok = this._promptService.promptAuth(this._window, aChannel,
  499.                                 aLevel, aAuthInfo, checkboxLabel, checkbox);
  500.  
  501.         // If there's a notification box, use it to allow the user to
  502.         // determine if the login should be saved. If there isn't a
  503.         // notification box, only save the login if the user set the
  504.         // checkbox to do so.
  505.         var rememberLogin = notifyBox ? canRememberLogin : checkbox.value;
  506.         if (!ok || !rememberLogin || epicfail)
  507.             return ok;
  508.  
  509.         try {
  510.             var [username, password] = this._GetAuthInfo(aAuthInfo);
  511.  
  512.             if (!password) {
  513.                 this.log("No password entered, so won't offer to save.");
  514.                 return ok;
  515.             }
  516.  
  517.             var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
  518.                            createInstance(Ci.nsILoginInfo);
  519.             newLogin.init(hostname, null, httpRealm,
  520.                           username, password, "", "");
  521.  
  522.             // XXX We can't prompt with multiple logins yet (bug 227632), so
  523.             // the entered login might correspond to an existing login
  524.             // other than the one we originally selected.
  525.             selectedLogin = this._repickSelectedLogin(foundLogins, username);
  526.  
  527.             // If we didn't find an existing login, or if the username
  528.             // changed, save as a new login.
  529.             if (!selectedLogin) {
  530.                 // add as new
  531.                 this.log("New login seen for " + username +
  532.                          " @ " + hostname + " (" + httpRealm + ")");
  533.                 if (notifyBox)
  534.                     this._showSaveLoginNotification(notifyBox, newLogin);
  535.                 else
  536.                     this._pwmgr.addLogin(newLogin);
  537.  
  538.             } else if (password != selectedLogin.password) {
  539.  
  540.                 this.log("Updating password for " + username +
  541.                          " @ " + hostname + " (" + httpRealm + ")");
  542.                 if (notifyBox)
  543.                     this._showChangeLoginNotification(notifyBox,
  544.                                                       selectedLogin, newLogin);
  545.                 else
  546.                     this._pwmgr.modifyLogin(selectedLogin, newLogin);
  547.  
  548.             } else {
  549.                 this.log("Login unchanged, no further action needed.");
  550.             }
  551.         } catch (e) {
  552.             Components.utils.reportError("LoginManagerPrompter: " +
  553.                 "Fail2 in promptAuth: " + e + "\n");
  554.         }
  555.  
  556.         return ok;
  557.     },
  558.  
  559.     asyncPromptAuth : function () {
  560.         return NS_ERROR_NOT_IMPLEMENTED;
  561.     },
  562.  
  563.  
  564.  
  565.  
  566.     /* ---------- nsILoginManagerPrompter prompts ---------- */
  567.  
  568.  
  569.  
  570.  
  571.     /*
  572.      * init
  573.      *
  574.      */
  575.     init : function (aWindow) {
  576.         this._window = aWindow;
  577.  
  578.         var prefBranch = Cc["@mozilla.org/preferences-service;1"].
  579.                          getService(Ci.nsIPrefService).getBranch("signon.");
  580.         this._debug = prefBranch.getBoolPref("debug");
  581.         this.log("===== initialized =====");
  582.     },
  583.  
  584.  
  585.     /*
  586.      * promptToSavePassword
  587.      *
  588.      */
  589.     promptToSavePassword : function (aLogin) {
  590.         var notifyBox = this._getNotifyBox();
  591.  
  592.         if (notifyBox)
  593.             this._showSaveLoginNotification(notifyBox, aLogin);
  594.         else
  595.             this._showSaveLoginDialog(aLogin);
  596.     },
  597.  
  598.  
  599.     /*
  600.      * _showLoginNotification
  601.      *
  602.      * Displays a notification bar.
  603.      *
  604.      */
  605.     _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
  606.         var oldBar = aNotifyBox.getNotificationWithValue(aName);
  607.         const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
  608.  
  609.         this.log("Adding new " + aName + " notification bar");
  610.         var newBar = aNotifyBox.appendNotification(
  611.                                 aText, aName,
  612.                                 "chrome://mozapps/skin/passwordmgr/key.png",
  613.                                 priority, aButtons);
  614.  
  615.         // The page we're going to hasn't loaded yet, so we want to persist
  616.         // across the first location change.
  617.         newBar.persistence++;
  618.  
  619.         // Sites like Gmail perform a funky redirect dance before you end up
  620.         // at the post-authentication page. I don't see a good way to
  621.         // heuristically determine when to ignore such location changes, so
  622.         // we'll try ignoring location changes based on a time interval.
  623.         newBar.timeout = Date.now() + 20000; // 20 seconds
  624.  
  625.         if (oldBar) {
  626.             this.log("(...and removing old " + aName + " notification bar)");
  627.             aNotifyBox.removeNotification(oldBar);
  628.         }
  629.     },
  630.  
  631.  
  632.     /*
  633.      * _showSaveLoginNotification
  634.      *
  635.      * Displays a notification bar (rather than a popup), to allow the user to
  636.      * save the specified login. This allows the user to see the results of
  637.      * their login, and only save a login which they know worked.
  638.      *
  639.      */
  640.     _showSaveLoginNotification : function (aNotifyBox, aLogin) {
  641.  
  642.         // Ugh. We can't use the strings from the popup window, because they
  643.         // have the access key marked in the string (eg "Mo&zilla"), along
  644.         // with some weird rules for handling access keys that do not occur
  645.         // in the string, for L10N. See commonDialog.js's setLabelForNode().
  646.         var neverButtonText =
  647.               this._getLocalizedString("notifyBarNeverForSiteButtonText");
  648.         var neverButtonAccessKey =
  649.               this._getLocalizedString("notifyBarNeverForSiteButtonAccessKey");
  650.         var rememberButtonText =
  651.               this._getLocalizedString("notifyBarRememberButtonText");
  652.         var rememberButtonAccessKey =
  653.               this._getLocalizedString("notifyBarRememberButtonAccessKey");
  654.         var notNowButtonText =
  655.               this._getLocalizedString("notifyBarNotNowButtonText");
  656.         var notNowButtonAccessKey =
  657.               this._getLocalizedString("notifyBarNotNowButtonAccessKey");
  658.  
  659.         var brandShortName =
  660.               this._brandBundle.GetStringFromName("brandShortName");
  661.         var displayHost = this._getShortDisplayHost(aLogin.hostname);
  662.         var notificationText;
  663.         if (aLogin.username) {
  664.             var displayUser = this._sanitizeUsername(aLogin.username);
  665.             notificationText  = this._getLocalizedString(
  666.                                         "saveLoginText",
  667.                                         [brandShortName, displayUser, displayHost]);
  668.         } else {
  669.             notificationText  = this._getLocalizedString(
  670.                                         "saveLoginTextNoUsername",
  671.                                         [brandShortName, displayHost]);
  672.         }
  673.  
  674.         // The callbacks in |buttons| have a closure to access the variables
  675.         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
  676.         // without a getService() call.
  677.         var pwmgr = this._pwmgr;
  678.  
  679.  
  680.         var buttons = [
  681.             // "Remember" button
  682.             {
  683.                 label:     rememberButtonText,
  684.                 accessKey: rememberButtonAccessKey,
  685.                 popup:     null,
  686.                 callback: function(aNotificationBar, aButton) {
  687.                     pwmgr.addLogin(aLogin);
  688.                 }
  689.             },
  690.  
  691.             // "Never for this site" button
  692.             {
  693.                 label:     neverButtonText,
  694.                 accessKey: neverButtonAccessKey,
  695.                 popup:     null,
  696.                 callback: function(aNotificationBar, aButton) {
  697.                     pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
  698.                 }
  699.             },
  700.  
  701.             // "Not now" button
  702.             {
  703.                 label:     notNowButtonText,
  704.                 accessKey: notNowButtonAccessKey,
  705.                 popup:     null,
  706.                 callback:  function() { /* NOP */ } 
  707.             }
  708.         ];
  709.  
  710.         this._showLoginNotification(aNotifyBox, "password-save",
  711.              notificationText, buttons);
  712.     },
  713.  
  714.  
  715.     /*
  716.      * _removeLoginNotifications
  717.      *
  718.      */
  719.     _removeLoginNotifications : function (aNotifyBox) {
  720.         var oldBar = aNotifyBox.getNotificationWithValue("password-save");
  721.         if (oldBar) {
  722.             this.log("Removing save-password notification bar.");
  723.             aNotifyBox.removeNotification(oldBar);
  724.         }
  725.  
  726.         oldBar = aNotifyBox.getNotificationWithValue("password-change");
  727.         if (oldBar) {
  728.             this.log("Removing change-password notification bar.");
  729.             aNotifyBox.removeNotification(oldBar);
  730.         }
  731.     },
  732.  
  733.  
  734.     /*
  735.      * _showSaveLoginDialog
  736.      *
  737.      * Called when we detect a new login in a form submission,
  738.      * asks the user what to do.
  739.      *
  740.      */
  741.     _showSaveLoginDialog : function (aLogin) {
  742.         const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
  743.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
  744.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
  745.             (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
  746.  
  747.         var brandShortName =
  748.                 this._brandBundle.GetStringFromName("brandShortName");
  749.         var displayHost = this._getShortDisplayHost(aLogin.hostname);
  750.  
  751.         var dialogText;
  752.         if (aLogin.username) {
  753.             var displayUser = this._sanitizeUsername(aLogin.username);
  754.             dialogText = this._getLocalizedString(
  755.                                  "saveLoginText",
  756.                                  [brandShortName, displayUser, displayHost]);
  757.         } else {
  758.             dialogText = this._getLocalizedString(
  759.                                  "saveLoginTextNoUsername",
  760.                                  [brandShortName, displayHost]);
  761.         }
  762.         var dialogTitle        = this._getLocalizedString(
  763.                                         "savePasswordTitle");
  764.         var neverButtonText    = this._getLocalizedString(
  765.                                         "neverForSiteButtonText");
  766.         var rememberButtonText = this._getLocalizedString(
  767.                                         "rememberButtonText");
  768.         var notNowButtonText   = this._getLocalizedString(
  769.                                         "notNowButtonText");
  770.  
  771.         this.log("Prompting user to save/ignore login");
  772.         var userChoice = this._promptService.confirmEx(this._window,
  773.                                             dialogTitle, dialogText,
  774.                                             buttonFlags, rememberButtonText,
  775.                                             notNowButtonText, neverButtonText,
  776.                                             null, {});
  777.         //  Returns:
  778.         //   0 - Save the login
  779.         //   1 - Ignore the login this time
  780.         //   2 - Never save logins for this site
  781.         if (userChoice == 2) {
  782.             this.log("Disabling " + aLogin.hostname + " logins by request.");
  783.             this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
  784.         } else if (userChoice == 0) {
  785.             this.log("Saving login for " + aLogin.hostname);
  786.             this._pwmgr.addLogin(aLogin);
  787.         } else {
  788.             // userChoice == 1 --> just ignore the login.
  789.             this.log("Ignoring login.");
  790.         }
  791.     },
  792.  
  793.  
  794.     /*
  795.      * promptToChangePassword
  796.      *
  797.      * Called when we think we detect a password change for an existing
  798.      * login, when the form being submitted contains multiple password
  799.      * fields.
  800.      *
  801.      */
  802.     promptToChangePassword : function (aOldLogin, aNewLogin) {
  803.         var notifyBox = this._getNotifyBox();
  804.  
  805.         if (notifyBox)
  806.             this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin);
  807.         else
  808.             this._showChangeLoginDialog(aOldLogin, aNewLogin);
  809.     },
  810.  
  811.  
  812.     /*
  813.      * _showChangeLoginNotification
  814.      *
  815.      * Shows the Change Password notification bar.
  816.      *
  817.      */
  818.     _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewLogin) {
  819.         var notificationText;
  820.         if (aOldLogin.username)
  821.             notificationText  = this._getLocalizedString(
  822.                                           "passwordChangeText",
  823.                                           [aOldLogin.username]);
  824.         else
  825.             notificationText  = this._getLocalizedString(
  826.                                           "passwordChangeTextNoUser");
  827.  
  828.         var changeButtonText =
  829.               this._getLocalizedString("notifyBarChangeButtonText");
  830.         var changeButtonAccessKey =
  831.               this._getLocalizedString("notifyBarChangeButtonAccessKey");
  832.         var dontChangeButtonText =
  833.               this._getLocalizedString("notifyBarDontChangeButtonText");
  834.         var dontChangeButtonAccessKey =
  835.               this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
  836.  
  837.         // The callbacks in |buttons| have a closure to access the variables
  838.         // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
  839.         // without a getService() call.
  840.         var pwmgr = this._pwmgr;
  841.  
  842.         var buttons = [
  843.             // "Yes" button
  844.             {
  845.                 label:     changeButtonText,
  846.                 accessKey: changeButtonAccessKey,
  847.                 popup:     null,
  848.                 callback:  function(aNotificationBar, aButton) {
  849.                     pwmgr.modifyLogin(aOldLogin, aNewLogin);
  850.                 }
  851.             },
  852.  
  853.             // "No" button
  854.             {
  855.                 label:     dontChangeButtonText,
  856.                 accessKey: dontChangeButtonAccessKey,
  857.                 popup:     null,
  858.                 callback:  function(aNotificationBar, aButton) {
  859.                     // do nothing
  860.                 }
  861.             }
  862.         ];
  863.  
  864.         this._showLoginNotification(aNotifyBox, "password-change",
  865.              notificationText, buttons);
  866.     },
  867.  
  868.  
  869.     /*
  870.      * _showChangeLoginDialog
  871.      *
  872.      * Shows the Change Password dialog.
  873.      *
  874.      */
  875.     _showChangeLoginDialog : function (aOldLogin, aNewLogin) {
  876.         const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  877.  
  878.         var dialogText;
  879.         if (aOldLogin.username)
  880.             dialogText  = this._getLocalizedString(
  881.                                     "passwordChangeText",
  882.                                     [aOldLogin.username]);
  883.         else
  884.             dialogText  = this._getLocalizedString(
  885.                                     "passwordChangeTextNoUser");
  886.  
  887.         var dialogTitle = this._getLocalizedString(
  888.                                     "passwordChangeTitle");
  889.  
  890.         // returns 0 for yes, 1 for no.
  891.         var ok = !this._promptService.confirmEx(this._window,
  892.                                 dialogTitle, dialogText, buttonFlags,
  893.                                 null, null, null,
  894.                                 null, {});
  895.         if (ok) {
  896.             this.log("Updating password for user " + aOldLogin.username);
  897.             this._pwmgr.modifyLogin(aOldLogin, aNewLogin);
  898.         }
  899.     },
  900.  
  901.  
  902.     /*
  903.      * promptToChangePasswordWithUsernames
  904.      *
  905.      * Called when we detect a password change in a form submission, but we
  906.      * don't know which existing login (username) it's for. Asks the user
  907.      * to select a username and confirm the password change.
  908.      *
  909.      * Note: The caller doesn't know the username for aNewLogin, so this
  910.      *       function fills in .username and .usernameField with the values
  911.      *       from the login selected by the user.
  912.      * 
  913.      * Note; XPCOM stupidity: |count| is just |logins.length|.
  914.      */
  915.     promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
  916.         const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  917.  
  918.         var usernames = logins.map(function (l) l.username);
  919.         var dialogText  = this._getLocalizedString("userSelectText");
  920.         var dialogTitle = this._getLocalizedString("passwordChangeTitle");
  921.         var selectedIndex = { value: null };
  922.  
  923.         // If user selects ok, outparam.value is set to the index
  924.         // of the selected username.
  925.         var ok = this._promptService.select(this._window,
  926.                                 dialogTitle, dialogText,
  927.                                 usernames.length, usernames,
  928.                                 selectedIndex);
  929.         if (ok) {
  930.             // Now that we know which login to change the password for,
  931.             // update the missing username info in the aNewLogin.
  932.  
  933.             var selectedLogin = logins[selectedIndex.value];
  934.  
  935.             this.log("Updating password for user " + selectedLogin.username);
  936.  
  937.             aNewLogin.username      = selectedLogin.username;
  938.             aNewLogin.usernameField = selectedLogin.usernameField;
  939.  
  940.             this._pwmgr.modifyLogin(selectedLogin, aNewLogin);
  941.         }
  942.     },
  943.  
  944.  
  945.  
  946.  
  947.     /* ---------- Internal Methods ---------- */
  948.  
  949.  
  950.  
  951.  
  952.     /*
  953.      * _getNotifyBox
  954.      *
  955.      * Returns the notification box to this prompter, or null if there isn't
  956.      * a notification box available.
  957.      */
  958.     _getNotifyBox : function () {
  959.         var notifyBox = null;
  960.  
  961.         // Given a content DOM window, returns the chrome window it's in.
  962.         function getChromeWindow(aWindow) {
  963.             var chromeWin = aWindow 
  964.                                 .QueryInterface(Ci.nsIInterfaceRequestor)
  965.                                 .getInterface(Ci.nsIWebNavigation)
  966.                                 .QueryInterface(Ci.nsIDocShellTreeItem)
  967.                                 .rootTreeItem
  968.                                 .QueryInterface(Ci.nsIInterfaceRequestor)
  969.                                 .getInterface(Ci.nsIDOMWindow)
  970.                                 .QueryInterface(Ci.nsIDOMChromeWindow);
  971.             return chromeWin;
  972.         }
  973.  
  974.         try {
  975.             // Get topmost window, in case we're in a frame.
  976.             var notifyWindow = this._window.top
  977.  
  978.             // Some sites pop up a temporary login window, when disappears
  979.             // upon submission of credentials. We want to put the notification
  980.             // bar in the opener window if this seems to be happening.
  981.             if (notifyWindow.opener) {
  982.                 var chromeDoc = getChromeWindow(notifyWindow)
  983.                                     .document.documentElement;
  984.                 var webnav = notifyWindow
  985.                                     .QueryInterface(Ci.nsIInterfaceRequestor)
  986.                                     .getInterface(Ci.nsIWebNavigation);
  987.  
  988.                 // Check to see if the current window was opened with chrome
  989.                 // disabled, and if so use the opener window. But if the window
  990.                 // has been used to visit other pages (ie, has a history),
  991.                 // assume it'll stick around and *don't* use the opener.
  992.                 if (chromeDoc.getAttribute("chromehidden") &&
  993.                     webnav.sessionHistory.count == 1) {
  994.                     this.log("Using opener window for notification bar.");
  995.                     notifyWindow = notifyWindow.opener;
  996.                 }
  997.             }
  998.  
  999.  
  1000.             // Get the chrome window for the content window we're using.
  1001.             // .wrappedJSObject needed here -- see bug 422974 comment 5.
  1002.             var chromeWin = getChromeWindow(notifyWindow).wrappedJSObject;
  1003.  
  1004.             if (chromeWin.getNotificationBox)
  1005.                 notifyBox = chromeWin.getNotificationBox(notifyWindow);
  1006.             else
  1007.                 this.log("getNotificationBox() not available on window");
  1008.  
  1009.         } catch (e) {
  1010.             // If any errors happen, just assume no notification box.
  1011.             this.log("No notification box available: " + e)
  1012.         }
  1013.  
  1014.         return notifyBox;
  1015.     },
  1016.  
  1017.  
  1018.     /*
  1019.      * _repickSelectedLogin
  1020.      *
  1021.      * The user might enter a login that isn't the one we prefilled, but
  1022.      * is the same as some other existing login. So, pick a login with a
  1023.      * matching username, or return null.
  1024.      */
  1025.     _repickSelectedLogin : function (foundLogins, username) {
  1026.         for (var i = 0; i < foundLogins.length; i++)
  1027.             if (foundLogins[i].username == username)
  1028.                 return foundLogins[i];
  1029.         return null;
  1030.     },
  1031.  
  1032.     
  1033.     /*
  1034.      * _getLocalizedString
  1035.      *
  1036.      * Can be called as:
  1037.      *   _getLocalizedString("key1");
  1038.      *   _getLocalizedString("key2", ["arg1"]);
  1039.      *   _getLocalizedString("key3", ["arg1", "arg2"]);
  1040.      *   (etc)
  1041.      *
  1042.      * Returns the localized string for the specified key,
  1043.      * formatted if required.
  1044.      *
  1045.      */ 
  1046.     _getLocalizedString : function (key, formatArgs) {
  1047.         if (formatArgs)
  1048.             return this._strBundle.formatStringFromName(
  1049.                                         key, formatArgs, formatArgs.length);
  1050.         else
  1051.             return this._strBundle.GetStringFromName(key);
  1052.     },
  1053.  
  1054.  
  1055.     /*
  1056.      * _sanitizeUsername
  1057.      *
  1058.      * Sanitizes the specified username, by stripping quotes and truncating if
  1059.      * it's too long. This helps prevent an evil site from messing with the
  1060.      * "save password?" prompt too much.
  1061.      */
  1062.     _sanitizeUsername : function (username) {
  1063.         if (username.length > 30) {
  1064.             username = username.substring(0, 30);
  1065.             username += this._ellipsis;
  1066.         }
  1067.         return username.replace(/['"]/g, "");
  1068.     },
  1069.  
  1070.  
  1071.     /*
  1072.      * _getFormattedHostname
  1073.      *
  1074.      * The aURI parameter may either be a string uri, or an nsIURI instance.
  1075.      *
  1076.      * Returns the hostname to use in a nsILoginInfo object (for example,
  1077.      * "http://example.com").
  1078.      */
  1079.     _getFormattedHostname : function (aURI) {
  1080.         var uri;
  1081.         if (aURI instanceof Ci.nsIURI) {
  1082.             uri = aURI;
  1083.         } else {
  1084.             uri = this._ioService.newURI(aURI, null, null);
  1085.         }
  1086.         var scheme = uri.scheme;
  1087.  
  1088.         var hostname = scheme + "://" + uri.host;
  1089.  
  1090.         // If the URI explicitly specified a port, only include it when
  1091.         // it's not the default. (We never want "http://foo.com:80")
  1092.         port = uri.port;
  1093.         if (port != -1) {
  1094.             var handler = this._ioService.getProtocolHandler(scheme);
  1095.             if (port != handler.defaultPort)
  1096.                 hostname += ":" + port;
  1097.         }
  1098.  
  1099.         return hostname;
  1100.     },
  1101.  
  1102.  
  1103.     /*
  1104.      * _getShortDisplayHost
  1105.      *
  1106.      * Converts a login's hostname field (a URL) to a short string for
  1107.      * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
  1108.      * "ftp://www.site.co.uk" --> "site.co.uk".
  1109.      */
  1110.     _getShortDisplayHost: function (aURIString) {
  1111.         var displayHost;
  1112.  
  1113.         var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
  1114.                           getService(Ci.nsIEffectiveTLDService);
  1115.         var idnService = Cc["@mozilla.org/network/idn-service;1"].
  1116.                          getService(Ci.nsIIDNService);
  1117.         try {
  1118.             var uri = this._ioService.newURI(aURIString, null, null);
  1119.             var baseDomain = eTLDService.getBaseDomain(uri);
  1120.             displayHost = idnService.convertToDisplayIDN(baseDomain, {});
  1121.         } catch (e) {
  1122.             this.log("_getShortDisplayHost couldn't process " + aURIString);
  1123.         }
  1124.  
  1125.         if (!displayHost)
  1126.             displayHost = aURIString;
  1127.  
  1128.         return displayHost;
  1129.     },
  1130.  
  1131.  
  1132.     /*
  1133.      * _getAuthTarget
  1134.      *
  1135.      * Returns the hostname and realm for which authentication is being
  1136.      * requested, in the format expected to be used with nsILoginInfo.
  1137.      */
  1138.     _getAuthTarget : function (aChannel, aAuthInfo) {
  1139.         var hostname, realm;
  1140.  
  1141.         // If our proxy is demanding authentication, don't use the
  1142.         // channel's actual destination.
  1143.         if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
  1144.             this.log("getAuthTarget is for proxy auth");
  1145.             if (!(aChannel instanceof Ci.nsIProxiedChannel))
  1146.                 throw "proxy auth needs nsIProxiedChannel";
  1147.  
  1148.             var info = aChannel.proxyInfo;
  1149.             if (!info)
  1150.                 throw "proxy auth needs nsIProxyInfo";
  1151.  
  1152.             // Proxies don't have a scheme, but we'll use "moz-proxy://"
  1153.             // so that it's more obvious what the login is for.
  1154.             var idnService = Cc["@mozilla.org/network/idn-service;1"].
  1155.                              getService(Ci.nsIIDNService);
  1156.             hostname = "moz-proxy://" +
  1157.                         idnService.convertUTF8toACE(info.host) +
  1158.                         ":" + info.port;
  1159.             realm = aAuthInfo.realm;
  1160.             if (!realm)
  1161.                 realm = hostname;
  1162.  
  1163.             return [hostname, realm];
  1164.         }
  1165.  
  1166.         hostname = this._getFormattedHostname(aChannel.URI);
  1167.  
  1168.         // If a HTTP WWW-Authenticate header specified a realm, that value
  1169.         // will be available here. If it wasn't set or wasn't HTTP, we'll use
  1170.         // the formatted hostname instead.
  1171.         realm = aAuthInfo.realm;
  1172.         if (!realm)
  1173.             realm = hostname;
  1174.  
  1175.         return [hostname, realm];
  1176.     },
  1177.  
  1178.  
  1179.     /**
  1180.      * Returns [username, password] as extracted from aAuthInfo (which
  1181.      * holds this info after having prompted the user).
  1182.      *
  1183.      * If the authentication was for a Windows domain, we'll prepend the
  1184.      * return username with the domain. (eg, "domain\user")
  1185.      */
  1186.     _GetAuthInfo : function (aAuthInfo) {
  1187.         var username, password;
  1188.  
  1189.         var flags = aAuthInfo.flags;
  1190.         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
  1191.             username = aAuthInfo.domain + "\\" + aAuthInfo.username;
  1192.         else
  1193.             username = aAuthInfo.username;
  1194.  
  1195.         password = aAuthInfo.password;
  1196.  
  1197.         return [username, password];
  1198.     },
  1199.  
  1200.  
  1201.     /**
  1202.      * Given a username (possibly in DOMAIN\user form) and password, parses the
  1203.      * domain out of the username if necessary and sets domain, username and
  1204.      * password on the auth information object.
  1205.      */
  1206.     _SetAuthInfo : function (aAuthInfo, username, password) {
  1207.         var flags = aAuthInfo.flags;
  1208.         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
  1209.             // Domain is separated from username by a backslash
  1210.             var idx = username.indexOf("\\");
  1211.             if (idx == -1) {
  1212.                 aAuthInfo.username = username;
  1213.             } else {
  1214.                 aAuthInfo.domain   =  username.substring(0, idx);
  1215.                 aAuthInfo.username =  username.substring(idx+1);
  1216.             }
  1217.         } else {
  1218.             aAuthInfo.username = username;
  1219.         }
  1220.         aAuthInfo.password = password;
  1221.     }
  1222.  
  1223. }; // end of LoginManagerPrompter implementation
  1224.  
  1225.  
  1226. var component = [LoginManagerPromptFactory, LoginManagerPrompter];
  1227. function NSGetModule(compMgr, fileSpec) {
  1228.     return XPCOMUtils.generateModule(component);
  1229. }
  1230.