toolkit/components/passwordmgr/nsLoginManagerPrompter.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1488 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +
     1.9 +const Cc = Components.classes;
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cr = Components.results;
    1.12 +
    1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.14 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.15 +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.16 +
    1.17 +/*
    1.18 + * LoginManagerPromptFactory
    1.19 + *
    1.20 + * Implements nsIPromptFactory
    1.21 + *
    1.22 + * Invoked by [toolkit/components/prompts/src/nsPrompter.js]
    1.23 + */
    1.24 +function LoginManagerPromptFactory() {
    1.25 +    Services.obs.addObserver(this, "quit-application-granted", true);
    1.26 +    Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
    1.27 +    Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
    1.28 +}
    1.29 +
    1.30 +LoginManagerPromptFactory.prototype = {
    1.31 +
    1.32 +    classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
    1.33 +    QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
    1.34 +
    1.35 +    _debug : false,
    1.36 +    _asyncPrompts : {},
    1.37 +    _asyncPromptInProgress : false,
    1.38 +
    1.39 +    observe : function (subject, topic, data) {
    1.40 +        this.log("Observed: " + topic);
    1.41 +        if (topic == "quit-application-granted") {
    1.42 +            this._cancelPendingPrompts();
    1.43 +        } else if (topic == "passwordmgr-crypto-login") {
    1.44 +            // Start processing the deferred prompters.
    1.45 +            this._doAsyncPrompt();
    1.46 +        } else if (topic == "passwordmgr-crypto-loginCanceled") {
    1.47 +            // User canceled a Master Password prompt, so go ahead and cancel
    1.48 +            // all pending auth prompts to avoid nagging over and over.
    1.49 +            this._cancelPendingPrompts();
    1.50 +        }
    1.51 +    },
    1.52 +
    1.53 +    getPrompt : function (aWindow, aIID) {
    1.54 +        var prefBranch = Services.prefs.getBranch("signon.");
    1.55 +        this._debug = prefBranch.getBoolPref("debug");
    1.56 +
    1.57 +        var prompt = new LoginManagerPrompter().QueryInterface(aIID);
    1.58 +        prompt.init(aWindow, this);
    1.59 +        return prompt;
    1.60 +    },
    1.61 +
    1.62 +    _doAsyncPrompt : function() {
    1.63 +        if (this._asyncPromptInProgress) {
    1.64 +            this.log("_doAsyncPrompt bypassed, already in progress");
    1.65 +            return;
    1.66 +        }
    1.67 +
    1.68 +        // Find the first prompt key we have in the queue
    1.69 +        var hashKey = null;
    1.70 +        for (hashKey in this._asyncPrompts)
    1.71 +            break;
    1.72 +
    1.73 +        if (!hashKey) {
    1.74 +            this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
    1.75 +            return;
    1.76 +        }
    1.77 +
    1.78 +        // If login manger has logins for this host, defer prompting if we're
    1.79 +        // already waiting on a master password entry.
    1.80 +        var prompt = this._asyncPrompts[hashKey];
    1.81 +        var prompter = prompt.prompter;
    1.82 +        var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo);
    1.83 +        var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0);
    1.84 +        if (hasLogins && prompter._pwmgr.uiBusy) {
    1.85 +            this.log("_doAsyncPrompt:run bypassed, master password UI busy");
    1.86 +            return;
    1.87 +        }
    1.88 +
    1.89 +        this._asyncPromptInProgress = true;
    1.90 +        prompt.inProgress = true;
    1.91 +
    1.92 +        var self = this;
    1.93 +
    1.94 +        var runnable = {
    1.95 +            run : function() {
    1.96 +                var ok = false;
    1.97 +                try {
    1.98 +                    self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
    1.99 +                    ok = prompter.promptAuth(prompt.channel,
   1.100 +                                             prompt.level,
   1.101 +                                             prompt.authInfo);
   1.102 +                } catch (e if (e instanceof Components.Exception) &&
   1.103 +                               e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
   1.104 +                    self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
   1.105 +                } catch (e) {
   1.106 +                    Components.utils.reportError("LoginManagerPrompter: " +
   1.107 +                        "_doAsyncPrompt:run: " + e + "\n");
   1.108 +                }
   1.109 +
   1.110 +                delete self._asyncPrompts[hashKey];
   1.111 +                prompt.inProgress = false;
   1.112 +                self._asyncPromptInProgress = false;
   1.113 +
   1.114 +                for each (var consumer in prompt.consumers) {
   1.115 +                    if (!consumer.callback)
   1.116 +                        // Not having a callback means that consumer didn't provide it
   1.117 +                        // or canceled the notification
   1.118 +                        continue;
   1.119 +
   1.120 +                    self.log("Calling back to " + consumer.callback + " ok=" + ok);
   1.121 +                    try {
   1.122 +                        if (ok)
   1.123 +                            consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
   1.124 +                        else
   1.125 +                            consumer.callback.onAuthCancelled(consumer.context, true);
   1.126 +                    } catch (e) { /* Throw away exceptions caused by callback */ }
   1.127 +                }
   1.128 +                self._doAsyncPrompt();
   1.129 +            }
   1.130 +        }
   1.131 +
   1.132 +        Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
   1.133 +        this.log("_doAsyncPrompt:run dispatched");
   1.134 +    },
   1.135 +
   1.136 +
   1.137 +    _cancelPendingPrompts : function() {
   1.138 +        this.log("Canceling all pending prompts...");
   1.139 +        var asyncPrompts = this._asyncPrompts;
   1.140 +        this.__proto__._asyncPrompts = {};
   1.141 +
   1.142 +        for each (var prompt in asyncPrompts) {
   1.143 +            // Watch out! If this prompt is currently prompting, let it handle
   1.144 +            // notifying the callbacks of success/failure, since it's already
   1.145 +            // asking the user for input. Reusing a callback can be crashy.
   1.146 +            if (prompt.inProgress) {
   1.147 +                this.log("skipping a prompt in progress");
   1.148 +                continue;
   1.149 +            }
   1.150 +
   1.151 +            for each (var consumer in prompt.consumers) {
   1.152 +                if (!consumer.callback)
   1.153 +                    continue;
   1.154 +
   1.155 +                this.log("Canceling async auth prompt callback " + consumer.callback);
   1.156 +                try {
   1.157 +                    consumer.callback.onAuthCancelled(consumer.context, true);
   1.158 +                } catch (e) { /* Just ignore exceptions from the callback */ }
   1.159 +            }
   1.160 +        }
   1.161 +    },
   1.162 +
   1.163 +
   1.164 +    log : function (message) {
   1.165 +        if (!this._debug)
   1.166 +            return;
   1.167 +
   1.168 +        dump("Pwmgr PromptFactory: " + message + "\n");
   1.169 +        Services.console.logStringMessage("Pwmgr PrompFactory: " + message);
   1.170 +    }
   1.171 +}; // end of LoginManagerPromptFactory implementation
   1.172 +
   1.173 +
   1.174 +
   1.175 +
   1.176 +/* ==================== LoginManagerPrompter ==================== */
   1.177 +
   1.178 +
   1.179 +
   1.180 +
   1.181 +/*
   1.182 + * LoginManagerPrompter
   1.183 + *
   1.184 + * Implements interfaces for prompting the user to enter/save/change auth info.
   1.185 + *
   1.186 + * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
   1.187 + *
   1.188 + * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
   1.189 + * (eg HTTP Authenticate, FTP login).
   1.190 + *
   1.191 + * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
   1.192 + * found in HTML forms.
   1.193 + */
   1.194 +function LoginManagerPrompter() {}
   1.195 +
   1.196 +LoginManagerPrompter.prototype = {
   1.197 +
   1.198 +    classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
   1.199 +    QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
   1.200 +                                            Ci.nsIAuthPrompt2,
   1.201 +                                            Ci.nsILoginManagerPrompter]),
   1.202 +
   1.203 +    _factory       : null,
   1.204 +    _window        : null,
   1.205 +    _debug         : false, // mirrors signon.debug
   1.206 +
   1.207 +    __pwmgr : null, // Password Manager service
   1.208 +    get _pwmgr() {
   1.209 +        if (!this.__pwmgr)
   1.210 +            this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
   1.211 +                           getService(Ci.nsILoginManager);
   1.212 +        return this.__pwmgr;
   1.213 +    },
   1.214 +
   1.215 +    __promptService : null, // Prompt service for user interaction
   1.216 +    get _promptService() {
   1.217 +        if (!this.__promptService)
   1.218 +            this.__promptService =
   1.219 +                Cc["@mozilla.org/embedcomp/prompt-service;1"].
   1.220 +                getService(Ci.nsIPromptService2);
   1.221 +        return this.__promptService;
   1.222 +    },
   1.223 +
   1.224 +
   1.225 +    __strBundle : null, // String bundle for L10N
   1.226 +    get _strBundle() {
   1.227 +        if (!this.__strBundle) {
   1.228 +            var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
   1.229 +                             getService(Ci.nsIStringBundleService);
   1.230 +            this.__strBundle = bunService.createBundle(
   1.231 +                        "chrome://passwordmgr/locale/passwordmgr.properties");
   1.232 +            if (!this.__strBundle)
   1.233 +                throw "String bundle for Login Manager not present!";
   1.234 +        }
   1.235 +
   1.236 +        return this.__strBundle;
   1.237 +    },
   1.238 +
   1.239 +
   1.240 +    __ellipsis : null,
   1.241 +    get _ellipsis() {
   1.242 +        if (!this.__ellipsis) {
   1.243 +            this.__ellipsis = "\u2026";
   1.244 +            try {
   1.245 +                this.__ellipsis = Services.prefs.getComplexValue(
   1.246 +                                    "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
   1.247 +            } catch (e) { }
   1.248 +        }
   1.249 +        return this.__ellipsis;
   1.250 +    },
   1.251 +
   1.252 +
   1.253 +    // Whether we are in private browsing mode
   1.254 +    get _inPrivateBrowsing() {
   1.255 +      if (this._window) {
   1.256 +        return PrivateBrowsingUtils.isWindowPrivate(this._window);
   1.257 +      } else {
   1.258 +        // If we don't that we're in private browsing mode if the caller did
   1.259 +        // not provide a window.  The callers which really care about this
   1.260 +        // will indeed pass down a window to us, and for those who don't,
   1.261 +        // we can just assume that we don't want to save the entered login
   1.262 +        // information.
   1.263 +        return true;
   1.264 +      }
   1.265 +    },
   1.266 +
   1.267 +
   1.268 +    /*
   1.269 +     * log
   1.270 +     *
   1.271 +     * Internal function for logging debug messages to the Error Console window.
   1.272 +     */
   1.273 +    log : function (message) {
   1.274 +        if (!this._debug)
   1.275 +            return;
   1.276 +
   1.277 +        dump("Pwmgr Prompter: " + message + "\n");
   1.278 +        Services.console.logStringMessage("Pwmgr Prompter: " + message);
   1.279 +    },
   1.280 +
   1.281 +
   1.282 +
   1.283 +
   1.284 +    /* ---------- nsIAuthPrompt prompts ---------- */
   1.285 +
   1.286 +
   1.287 +    /*
   1.288 +     * prompt
   1.289 +     *
   1.290 +     * Wrapper around the prompt service prompt. Saving random fields here
   1.291 +     * doesn't really make sense and therefore isn't implemented.
   1.292 +     */
   1.293 +    prompt : function (aDialogTitle, aText, aPasswordRealm,
   1.294 +                       aSavePassword, aDefaultText, aResult) {
   1.295 +        if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
   1.296 +            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   1.297 +
   1.298 +        this.log("===== prompt() called =====");
   1.299 +
   1.300 +        if (aDefaultText) {
   1.301 +            aResult.value = aDefaultText;
   1.302 +        }
   1.303 +
   1.304 +        return this._promptService.prompt(this._window,
   1.305 +               aDialogTitle, aText, aResult, null, {});
   1.306 +    },
   1.307 +
   1.308 +
   1.309 +    /*
   1.310 +     * promptUsernameAndPassword
   1.311 +     *
   1.312 +     * Looks up a username and password in the database. Will prompt the user
   1.313 +     * with a dialog, even if a username and password are found.
   1.314 +     */
   1.315 +    promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
   1.316 +                                         aSavePassword, aUsername, aPassword) {
   1.317 +        this.log("===== promptUsernameAndPassword() called =====");
   1.318 +
   1.319 +        if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
   1.320 +            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   1.321 +
   1.322 +        var selectedLogin = null;
   1.323 +        var checkBox = { value : false };
   1.324 +        var checkBoxLabel = null;
   1.325 +        var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
   1.326 +
   1.327 +        // If hostname is null, we can't save this login.
   1.328 +        if (hostname) {
   1.329 +            var canRememberLogin;
   1.330 +            if (this._inPrivateBrowsing)
   1.331 +                canRememberLogin = false;
   1.332 +            else
   1.333 +                canRememberLogin = (aSavePassword ==
   1.334 +                                    Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
   1.335 +                                   this._pwmgr.getLoginSavingEnabled(hostname);
   1.336 +
   1.337 +            // if checkBoxLabel is null, the checkbox won't be shown at all.
   1.338 +            if (canRememberLogin)
   1.339 +                checkBoxLabel = this._getLocalizedString("rememberPassword");
   1.340 +
   1.341 +            // Look for existing logins.
   1.342 +            var foundLogins = this._pwmgr.findLogins({}, hostname, null,
   1.343 +                                                     realm);
   1.344 +
   1.345 +            // XXX Like the original code, we can't deal with multiple
   1.346 +            // account selection. (bug 227632)
   1.347 +            if (foundLogins.length > 0) {
   1.348 +                selectedLogin = foundLogins[0];
   1.349 +
   1.350 +                // If the caller provided a username, try to use it. If they
   1.351 +                // provided only a password, this will try to find a password-only
   1.352 +                // login (or return null if none exists).
   1.353 +                if (aUsername.value)
   1.354 +                    selectedLogin = this._repickSelectedLogin(foundLogins,
   1.355 +                                                              aUsername.value);
   1.356 +
   1.357 +                if (selectedLogin) {
   1.358 +                    checkBox.value = true;
   1.359 +                    aUsername.value = selectedLogin.username;
   1.360 +                    // If the caller provided a password, prefer it.
   1.361 +                    if (!aPassword.value)
   1.362 +                        aPassword.value = selectedLogin.password;
   1.363 +                }
   1.364 +            }
   1.365 +        }
   1.366 +
   1.367 +        var ok = this._promptService.promptUsernameAndPassword(this._window,
   1.368 +                    aDialogTitle, aText, aUsername, aPassword,
   1.369 +                    checkBoxLabel, checkBox);
   1.370 +
   1.371 +        if (!ok || !checkBox.value || !hostname)
   1.372 +            return ok;
   1.373 +
   1.374 +        if (!aPassword.value) {
   1.375 +            this.log("No password entered, so won't offer to save.");
   1.376 +            return ok;
   1.377 +        }
   1.378 +
   1.379 +        // XXX We can't prompt with multiple logins yet (bug 227632), so
   1.380 +        // the entered login might correspond to an existing login
   1.381 +        // other than the one we originally selected.
   1.382 +        selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
   1.383 +
   1.384 +        // If we didn't find an existing login, or if the username
   1.385 +        // changed, save as a new login.
   1.386 +        if (!selectedLogin) {
   1.387 +            // add as new
   1.388 +            this.log("New login seen for " + realm);
   1.389 +            var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
   1.390 +                           createInstance(Ci.nsILoginInfo);
   1.391 +            newLogin.init(hostname, null, realm,
   1.392 +                          aUsername.value, aPassword.value, "", "");
   1.393 +            this._pwmgr.addLogin(newLogin);
   1.394 +        } else if (aPassword.value != selectedLogin.password) {
   1.395 +            // update password
   1.396 +            this.log("Updating password for  " + realm);
   1.397 +            this._updateLogin(selectedLogin, aPassword.value);
   1.398 +        } else {
   1.399 +            this.log("Login unchanged, no further action needed.");
   1.400 +            this._updateLogin(selectedLogin);
   1.401 +        }
   1.402 +
   1.403 +        return ok;
   1.404 +    },
   1.405 +
   1.406 +
   1.407 +    /*
   1.408 +     * promptPassword
   1.409 +     *
   1.410 +     * If a password is found in the database for the password realm, it is
   1.411 +     * returned straight away without displaying a dialog.
   1.412 +     *
   1.413 +     * If a password is not found in the database, the user will be prompted
   1.414 +     * with a dialog with a text field and ok/cancel buttons. If the user
   1.415 +     * allows it, then the password will be saved in the database.
   1.416 +     */
   1.417 +    promptPassword : function (aDialogTitle, aText, aPasswordRealm,
   1.418 +                               aSavePassword, aPassword) {
   1.419 +        this.log("===== promptPassword called() =====");
   1.420 +
   1.421 +        if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
   1.422 +            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   1.423 +
   1.424 +        var checkBox = { value : false };
   1.425 +        var checkBoxLabel = null;
   1.426 +        var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
   1.427 +
   1.428 +        username = decodeURIComponent(username);
   1.429 +
   1.430 +        // If hostname is null, we can't save this login.
   1.431 +        if (hostname && !this._inPrivateBrowsing) {
   1.432 +          var canRememberLogin = (aSavePassword ==
   1.433 +                                  Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
   1.434 +                                 this._pwmgr.getLoginSavingEnabled(hostname);
   1.435 +
   1.436 +          // if checkBoxLabel is null, the checkbox won't be shown at all.
   1.437 +          if (canRememberLogin)
   1.438 +              checkBoxLabel = this._getLocalizedString("rememberPassword");
   1.439 +
   1.440 +          if (!aPassword.value) {
   1.441 +              // Look for existing logins.
   1.442 +              var foundLogins = this._pwmgr.findLogins({}, hostname, null,
   1.443 +                                                       realm);
   1.444 +
   1.445 +              // XXX Like the original code, we can't deal with multiple
   1.446 +              // account selection (bug 227632). We can deal with finding the
   1.447 +              // account based on the supplied username - but in this case we'll
   1.448 +              // just return the first match.
   1.449 +              for (var i = 0; i < foundLogins.length; ++i) {
   1.450 +                  if (foundLogins[i].username == username) {
   1.451 +                      aPassword.value = foundLogins[i].password;
   1.452 +                      // wallet returned straight away, so this mimics that code
   1.453 +                      return true;
   1.454 +                  }
   1.455 +              }
   1.456 +          }
   1.457 +        }
   1.458 +
   1.459 +        var ok = this._promptService.promptPassword(this._window, aDialogTitle,
   1.460 +                                                    aText, aPassword,
   1.461 +                                                    checkBoxLabel, checkBox);
   1.462 +
   1.463 +        if (ok && checkBox.value && hostname && aPassword.value) {
   1.464 +            var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
   1.465 +                           createInstance(Ci.nsILoginInfo);
   1.466 +            newLogin.init(hostname, null, realm, username,
   1.467 +                          aPassword.value, "", "");
   1.468 +
   1.469 +            this.log("New login seen for " + realm);
   1.470 +
   1.471 +            this._pwmgr.addLogin(newLogin);
   1.472 +        }
   1.473 +
   1.474 +        return ok;
   1.475 +    },
   1.476 +
   1.477 +    /* ---------- nsIAuthPrompt helpers ---------- */
   1.478 +
   1.479 +
   1.480 +    /**
   1.481 +     * Given aRealmString, such as "http://user@example.com/foo", returns an
   1.482 +     * array of:
   1.483 +     *   - the formatted hostname
   1.484 +     *   - the realm (hostname + path)
   1.485 +     *   - the username, if present
   1.486 +     *
   1.487 +     * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
   1.488 +     * channels, e.g. "example.com:80 (httprealm)", null is returned for all
   1.489 +     * arguments to let callers know the login can't be saved because we don't
   1.490 +     * know whether it's http or https.
   1.491 +     */
   1.492 +    _getRealmInfo : function (aRealmString) {
   1.493 +        var httpRealm = /^.+ \(.+\)$/;
   1.494 +        if (httpRealm.test(aRealmString))
   1.495 +            return [null, null, null];
   1.496 +
   1.497 +        var uri = Services.io.newURI(aRealmString, null, null);
   1.498 +        var pathname = "";
   1.499 +
   1.500 +        if (uri.path != "/")
   1.501 +            pathname = uri.path;
   1.502 +
   1.503 +        var formattedHostname = this._getFormattedHostname(uri);
   1.504 +
   1.505 +        return [formattedHostname, formattedHostname + pathname, uri.username];
   1.506 +    },
   1.507 +
   1.508 +    /* ---------- nsIAuthPrompt2 prompts ---------- */
   1.509 +
   1.510 +
   1.511 +
   1.512 +
   1.513 +    /*
   1.514 +     * promptAuth
   1.515 +     *
   1.516 +     * Implementation of nsIAuthPrompt2.
   1.517 +     *
   1.518 +     * nsIChannel aChannel
   1.519 +     * int        aLevel
   1.520 +     * nsIAuthInformation aAuthInfo
   1.521 +     */
   1.522 +    promptAuth : function (aChannel, aLevel, aAuthInfo) {
   1.523 +        var selectedLogin = null;
   1.524 +        var checkbox = { value : false };
   1.525 +        var checkboxLabel = null;
   1.526 +        var epicfail = false;
   1.527 +        var canAutologin = false;
   1.528 +
   1.529 +        try {
   1.530 +
   1.531 +            this.log("===== promptAuth called =====");
   1.532 +
   1.533 +            // If the user submits a login but it fails, we need to remove the
   1.534 +            // notification bar that was displayed. Conveniently, the user will
   1.535 +            // be prompted for authentication again, which brings us here.
   1.536 +            this._removeLoginNotifications();
   1.537 +
   1.538 +            var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
   1.539 +
   1.540 +
   1.541 +            // Looks for existing logins to prefill the prompt with.
   1.542 +            var foundLogins = this._pwmgr.findLogins({},
   1.543 +                                        hostname, null, httpRealm);
   1.544 +            this.log("found " + foundLogins.length + " matching logins.");
   1.545 +
   1.546 +            // XXX Can't select from multiple accounts yet. (bug 227632)
   1.547 +            if (foundLogins.length > 0) {
   1.548 +                selectedLogin = foundLogins[0];
   1.549 +                this._SetAuthInfo(aAuthInfo, selectedLogin.username,
   1.550 +                                             selectedLogin.password);
   1.551 +
   1.552 +                // Allow automatic proxy login
   1.553 +                if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
   1.554 +                    !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
   1.555 +                    Services.prefs.getBoolPref("signon.autologin.proxy") &&
   1.556 +                    !this._inPrivateBrowsing) {
   1.557 +
   1.558 +                    this.log("Autologin enabled, skipping auth prompt.");
   1.559 +                    canAutologin = true;
   1.560 +                }
   1.561 +
   1.562 +                checkbox.value = true;
   1.563 +            }
   1.564 +
   1.565 +            var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
   1.566 +            if (this._inPrivateBrowsing)
   1.567 +              canRememberLogin = false;
   1.568 +
   1.569 +            // if checkboxLabel is null, the checkbox won't be shown at all.
   1.570 +            var notifyBox = this._getNotifyBox();
   1.571 +            if (canRememberLogin && !notifyBox)
   1.572 +                checkboxLabel = this._getLocalizedString("rememberPassword");
   1.573 +        } catch (e) {
   1.574 +            // Ignore any errors and display the prompt anyway.
   1.575 +            epicfail = true;
   1.576 +            Components.utils.reportError("LoginManagerPrompter: " +
   1.577 +                "Epic fail in promptAuth: " + e + "\n");
   1.578 +        }
   1.579 +
   1.580 +        var ok = canAutologin ||
   1.581 +                 this._promptService.promptAuth(this._window,
   1.582 +                                                aChannel, aLevel, aAuthInfo,
   1.583 +                                                checkboxLabel, checkbox);
   1.584 +
   1.585 +        // If there's a notification box, use it to allow the user to
   1.586 +        // determine if the login should be saved. If there isn't a
   1.587 +        // notification box, only save the login if the user set the
   1.588 +        // checkbox to do so.
   1.589 +        var rememberLogin = notifyBox ? canRememberLogin : checkbox.value;
   1.590 +        if (!ok || !rememberLogin || epicfail)
   1.591 +            return ok;
   1.592 +
   1.593 +        try {
   1.594 +            var [username, password] = this._GetAuthInfo(aAuthInfo);
   1.595 +
   1.596 +            if (!password) {
   1.597 +                this.log("No password entered, so won't offer to save.");
   1.598 +                return ok;
   1.599 +            }
   1.600 +
   1.601 +            // XXX We can't prompt with multiple logins yet (bug 227632), so
   1.602 +            // the entered login might correspond to an existing login
   1.603 +            // other than the one we originally selected.
   1.604 +            selectedLogin = this._repickSelectedLogin(foundLogins, username);
   1.605 +
   1.606 +            // If we didn't find an existing login, or if the username
   1.607 +            // changed, save as a new login.
   1.608 +            if (!selectedLogin) {
   1.609 +                this.log("New login seen for " + username +
   1.610 +                         " @ " + hostname + " (" + httpRealm + ")");
   1.611 +
   1.612 +                var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
   1.613 +                               createInstance(Ci.nsILoginInfo);
   1.614 +                newLogin.init(hostname, null, httpRealm,
   1.615 +                              username, password, "", "");
   1.616 +                var notifyObj = this._getPopupNote() || notifyBox;
   1.617 +                if (notifyObj)
   1.618 +                    this._showSaveLoginNotification(notifyObj, newLogin);
   1.619 +                else
   1.620 +                    this._pwmgr.addLogin(newLogin);
   1.621 +
   1.622 +            } else if (password != selectedLogin.password) {
   1.623 +
   1.624 +                this.log("Updating password for " + username +
   1.625 +                         " @ " + hostname + " (" + httpRealm + ")");
   1.626 +                var notifyObj = this._getPopupNote() || notifyBox;
   1.627 +                if (notifyObj)
   1.628 +                    this._showChangeLoginNotification(notifyObj,
   1.629 +                                                      selectedLogin, password);
   1.630 +                else
   1.631 +                    this._updateLogin(selectedLogin, password);
   1.632 +
   1.633 +            } else {
   1.634 +                this.log("Login unchanged, no further action needed.");
   1.635 +                this._updateLogin(selectedLogin);
   1.636 +            }
   1.637 +        } catch (e) {
   1.638 +            Components.utils.reportError("LoginManagerPrompter: " +
   1.639 +                "Fail2 in promptAuth: " + e + "\n");
   1.640 +        }
   1.641 +
   1.642 +        return ok;
   1.643 +    },
   1.644 +
   1.645 +    asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
   1.646 +        var cancelable = null;
   1.647 +
   1.648 +        try {
   1.649 +            this.log("===== asyncPromptAuth called =====");
   1.650 +
   1.651 +            // If the user submits a login but it fails, we need to remove the
   1.652 +            // notification bar that was displayed. Conveniently, the user will
   1.653 +            // be prompted for authentication again, which brings us here.
   1.654 +            this._removeLoginNotifications();
   1.655 +
   1.656 +            cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
   1.657 +
   1.658 +            var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
   1.659 +
   1.660 +            var hashKey = aLevel + "|" + hostname + "|" + httpRealm;
   1.661 +            this.log("Async prompt key = " + hashKey);
   1.662 +            var asyncPrompt = this._factory._asyncPrompts[hashKey];
   1.663 +            if (asyncPrompt) {
   1.664 +                this.log("Prompt bound to an existing one in the queue, callback = " + aCallback);
   1.665 +                asyncPrompt.consumers.push(cancelable);
   1.666 +                return cancelable;
   1.667 +            }
   1.668 +
   1.669 +            this.log("Adding new prompt to the queue, callback = " + aCallback);
   1.670 +            asyncPrompt = {
   1.671 +                consumers: [cancelable],
   1.672 +                channel: aChannel,
   1.673 +                authInfo: aAuthInfo,
   1.674 +                level: aLevel,
   1.675 +                inProgress : false,
   1.676 +                prompter: this
   1.677 +            }
   1.678 +
   1.679 +            this._factory._asyncPrompts[hashKey] = asyncPrompt;
   1.680 +            this._factory._doAsyncPrompt();
   1.681 +        }
   1.682 +        catch (e) {
   1.683 +            Components.utils.reportError("LoginManagerPrompter: " +
   1.684 +                "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n");
   1.685 +            // Fail the prompt operation to let the consumer fall back
   1.686 +            // to synchronous promptAuth method
   1.687 +            throw e;
   1.688 +        }
   1.689 +
   1.690 +        return cancelable;
   1.691 +    },
   1.692 +
   1.693 +
   1.694 +
   1.695 +
   1.696 +    /* ---------- nsILoginManagerPrompter prompts ---------- */
   1.697 +
   1.698 +
   1.699 +
   1.700 +
   1.701 +    /*
   1.702 +     * init
   1.703 +     *
   1.704 +     */
   1.705 +    init : function (aWindow, aFactory) {
   1.706 +        this._window = aWindow;
   1.707 +        this._factory = aFactory || null;
   1.708 +
   1.709 +        var prefBranch = Services.prefs.getBranch("signon.");
   1.710 +        this._debug = prefBranch.getBoolPref("debug");
   1.711 +        this.log("===== initialized =====");
   1.712 +    },
   1.713 +
   1.714 +
   1.715 +    /*
   1.716 +     * promptToSavePassword
   1.717 +     *
   1.718 +     */
   1.719 +    promptToSavePassword : function (aLogin) {
   1.720 +        var notifyObj = this._getPopupNote() || this._getNotifyBox();
   1.721 +
   1.722 +        if (notifyObj)
   1.723 +            this._showSaveLoginNotification(notifyObj, aLogin);
   1.724 +        else
   1.725 +            this._showSaveLoginDialog(aLogin);
   1.726 +    },
   1.727 +
   1.728 +
   1.729 +    /*
   1.730 +     * _showLoginNotification
   1.731 +     *
   1.732 +     * Displays a notification bar.
   1.733 +     *
   1.734 +     */
   1.735 +    _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
   1.736 +        var oldBar = aNotifyBox.getNotificationWithValue(aName);
   1.737 +        const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
   1.738 +
   1.739 +        this.log("Adding new " + aName + " notification bar");
   1.740 +        var newBar = aNotifyBox.appendNotification(
   1.741 +                                aText, aName,
   1.742 +                                "chrome://mozapps/skin/passwordmgr/key.png",
   1.743 +                                priority, aButtons);
   1.744 +
   1.745 +        // The page we're going to hasn't loaded yet, so we want to persist
   1.746 +        // across the first location change.
   1.747 +        newBar.persistence++;
   1.748 +
   1.749 +        // Sites like Gmail perform a funky redirect dance before you end up
   1.750 +        // at the post-authentication page. I don't see a good way to
   1.751 +        // heuristically determine when to ignore such location changes, so
   1.752 +        // we'll try ignoring location changes based on a time interval.
   1.753 +        newBar.timeout = Date.now() + 20000; // 20 seconds
   1.754 +
   1.755 +        if (oldBar) {
   1.756 +            this.log("(...and removing old " + aName + " notification bar)");
   1.757 +            aNotifyBox.removeNotification(oldBar);
   1.758 +        }
   1.759 +    },
   1.760 +
   1.761 +
   1.762 +    /*
   1.763 +     * _showSaveLoginNotification
   1.764 +     *
   1.765 +     * Displays a notification bar or a popup notification, to allow the user
   1.766 +     * to save the specified login. This allows the user to see the results of
   1.767 +     * their login, and only save a login which they know worked.
   1.768 +     *
   1.769 +     * @param aNotifyObj
   1.770 +     *        A notification box or a popup notification.
   1.771 +     */
   1.772 +    _showSaveLoginNotification : function (aNotifyObj, aLogin) {
   1.773 +
   1.774 +        // Ugh. We can't use the strings from the popup window, because they
   1.775 +        // have the access key marked in the string (eg "Mo&zilla"), along
   1.776 +        // with some weird rules for handling access keys that do not occur
   1.777 +        // in the string, for L10N. See commonDialog.js's setLabelForNode().
   1.778 +        var neverButtonText =
   1.779 +              this._getLocalizedString("notifyBarNeverRememberButtonText");
   1.780 +        var neverButtonAccessKey =
   1.781 +              this._getLocalizedString("notifyBarNeverRememberButtonAccessKey");
   1.782 +        var rememberButtonText =
   1.783 +              this._getLocalizedString("notifyBarRememberPasswordButtonText");
   1.784 +        var rememberButtonAccessKey =
   1.785 +              this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
   1.786 +
   1.787 +        var displayHost = this._getShortDisplayHost(aLogin.hostname);
   1.788 +        var notificationText;
   1.789 +        if (aLogin.username) {
   1.790 +            var displayUser = this._sanitizeUsername(aLogin.username);
   1.791 +            notificationText  = this._getLocalizedString(
   1.792 +                                        "rememberPasswordMsg",
   1.793 +                                        [displayUser, displayHost]);
   1.794 +        } else {
   1.795 +            notificationText  = this._getLocalizedString(
   1.796 +                                        "rememberPasswordMsgNoUsername",
   1.797 +                                        [displayHost]);
   1.798 +        }
   1.799 +
   1.800 +        // The callbacks in |buttons| have a closure to access the variables
   1.801 +        // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
   1.802 +        // without a getService() call.
   1.803 +        var pwmgr = this._pwmgr;
   1.804 +
   1.805 +        // Notification is a PopupNotification
   1.806 +        if (aNotifyObj == this._getPopupNote()) {
   1.807 +            // "Remember" button
   1.808 +            var mainAction = {
   1.809 +                label:     rememberButtonText,
   1.810 +                accessKey: rememberButtonAccessKey,
   1.811 +                callback: function(aNotifyObj, aButton) {
   1.812 +                    pwmgr.addLogin(aLogin);
   1.813 +                    browser.focus();
   1.814 +                }
   1.815 +            };
   1.816 +
   1.817 +            var secondaryActions = [
   1.818 +                // "Never for this site" button
   1.819 +                {
   1.820 +                    label:     neverButtonText,
   1.821 +                    accessKey: neverButtonAccessKey,
   1.822 +                    callback: function(aNotifyObj, aButton) {
   1.823 +                        pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
   1.824 +                        browser.focus();
   1.825 +                    }
   1.826 +                }
   1.827 +            ];
   1.828 +
   1.829 +            var notifyWin = this._getNotifyWindow();
   1.830 +            var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
   1.831 +            var browser = chromeWin.gBrowser.
   1.832 +                                    getBrowserForDocument(notifyWin.top.document);
   1.833 +
   1.834 +            aNotifyObj.show(browser, "password-save", notificationText,
   1.835 +                            "password-notification-icon", mainAction,
   1.836 +                            secondaryActions, { timeout: Date.now() + 10000,
   1.837 +                                                persistWhileVisible: true });
   1.838 +        } else {
   1.839 +            var notNowButtonText =
   1.840 +                  this._getLocalizedString("notifyBarNotNowButtonText");
   1.841 +            var notNowButtonAccessKey =
   1.842 +                  this._getLocalizedString("notifyBarNotNowButtonAccessKey");
   1.843 +            var buttons = [
   1.844 +                // "Remember" button
   1.845 +                {
   1.846 +                    label:     rememberButtonText,
   1.847 +                    accessKey: rememberButtonAccessKey,
   1.848 +                    popup:     null,
   1.849 +                    callback: function(aNotifyObj, aButton) {
   1.850 +                        pwmgr.addLogin(aLogin);
   1.851 +                    }
   1.852 +                },
   1.853 +
   1.854 +                // "Never for this site" button
   1.855 +                {
   1.856 +                    label:     neverButtonText,
   1.857 +                    accessKey: neverButtonAccessKey,
   1.858 +                    popup:     null,
   1.859 +                    callback: function(aNotifyObj, aButton) {
   1.860 +                        pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
   1.861 +                    }
   1.862 +                },
   1.863 +
   1.864 +                // "Not now" button
   1.865 +                {
   1.866 +                    label:     notNowButtonText,
   1.867 +                    accessKey: notNowButtonAccessKey,
   1.868 +                    popup:     null,
   1.869 +                    callback:  function() { /* NOP */ }
   1.870 +                }
   1.871 +            ];
   1.872 +
   1.873 +            this._showLoginNotification(aNotifyObj, "password-save",
   1.874 +                                        notificationText, buttons);
   1.875 +        }
   1.876 +    },
   1.877 +
   1.878 +
   1.879 +    /*
   1.880 +     * _removeLoginNotifications
   1.881 +     *
   1.882 +     */
   1.883 +    _removeLoginNotifications : function () {
   1.884 +        var popupNote = this._getPopupNote();
   1.885 +        if (popupNote)
   1.886 +            popupNote = popupNote.getNotification("password-save");
   1.887 +        if (popupNote)
   1.888 +            popupNote.remove();
   1.889 +
   1.890 +        var notifyBox = this._getNotifyBox();
   1.891 +        if (notifyBox) {
   1.892 +            var oldBar = notifyBox.getNotificationWithValue("password-save");
   1.893 +            if (oldBar) {
   1.894 +                this.log("Removing save-password notification bar.");
   1.895 +                notifyBox.removeNotification(oldBar);
   1.896 +            }
   1.897 +
   1.898 +            oldBar = notifyBox.getNotificationWithValue("password-change");
   1.899 +            if (oldBar) {
   1.900 +                this.log("Removing change-password notification bar.");
   1.901 +                notifyBox.removeNotification(oldBar);
   1.902 +            }
   1.903 +        }
   1.904 +    },
   1.905 +
   1.906 +
   1.907 +    /*
   1.908 +     * _showSaveLoginDialog
   1.909 +     *
   1.910 +     * Called when we detect a new login in a form submission,
   1.911 +     * asks the user what to do.
   1.912 +     *
   1.913 +     */
   1.914 +    _showSaveLoginDialog : function (aLogin) {
   1.915 +        const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
   1.916 +            (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
   1.917 +            (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
   1.918 +            (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
   1.919 +
   1.920 +        var displayHost = this._getShortDisplayHost(aLogin.hostname);
   1.921 +
   1.922 +        var dialogText;
   1.923 +        if (aLogin.username) {
   1.924 +            var displayUser = this._sanitizeUsername(aLogin.username);
   1.925 +            dialogText = this._getLocalizedString(
   1.926 +                                 "rememberPasswordMsg",
   1.927 +                                 [displayUser, displayHost]);
   1.928 +        } else {
   1.929 +            dialogText = this._getLocalizedString(
   1.930 +                                 "rememberPasswordMsgNoUsername",
   1.931 +                                 [displayHost]);
   1.932 +
   1.933 +        }
   1.934 +        var dialogTitle        = this._getLocalizedString(
   1.935 +                                        "savePasswordTitle");
   1.936 +        var neverButtonText    = this._getLocalizedString(
   1.937 +                                        "neverForSiteButtonText");
   1.938 +        var rememberButtonText = this._getLocalizedString(
   1.939 +                                        "rememberButtonText");
   1.940 +        var notNowButtonText   = this._getLocalizedString(
   1.941 +                                        "notNowButtonText");
   1.942 +
   1.943 +        this.log("Prompting user to save/ignore login");
   1.944 +        var userChoice = this._promptService.confirmEx(this._window,
   1.945 +                                            dialogTitle, dialogText,
   1.946 +                                            buttonFlags, rememberButtonText,
   1.947 +                                            notNowButtonText, neverButtonText,
   1.948 +                                            null, {});
   1.949 +        //  Returns:
   1.950 +        //   0 - Save the login
   1.951 +        //   1 - Ignore the login this time
   1.952 +        //   2 - Never save logins for this site
   1.953 +        if (userChoice == 2) {
   1.954 +            this.log("Disabling " + aLogin.hostname + " logins by request.");
   1.955 +            this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
   1.956 +        } else if (userChoice == 0) {
   1.957 +            this.log("Saving login for " + aLogin.hostname);
   1.958 +            this._pwmgr.addLogin(aLogin);
   1.959 +        } else {
   1.960 +            // userChoice == 1 --> just ignore the login.
   1.961 +            this.log("Ignoring login.");
   1.962 +        }
   1.963 +    },
   1.964 +
   1.965 +
   1.966 +    /*
   1.967 +     * promptToChangePassword
   1.968 +     *
   1.969 +     * Called when we think we detect a password change for an existing
   1.970 +     * login, when the form being submitted contains multiple password
   1.971 +     * fields.
   1.972 +     *
   1.973 +     */
   1.974 +    promptToChangePassword : function (aOldLogin, aNewLogin) {
   1.975 +        var notifyObj = this._getPopupNote() || this._getNotifyBox();
   1.976 +
   1.977 +        if (notifyObj)
   1.978 +            this._showChangeLoginNotification(notifyObj, aOldLogin,
   1.979 +                                              aNewLogin.password);
   1.980 +        else
   1.981 +            this._showChangeLoginDialog(aOldLogin, aNewLogin.password);
   1.982 +    },
   1.983 +
   1.984 +
   1.985 +    /*
   1.986 +     * _showChangeLoginNotification
   1.987 +     *
   1.988 +     * Shows the Change Password notification bar or popup notification.
   1.989 +     *
   1.990 +     * @param aNotifyObj
   1.991 +     *        A notification box or a popup notification.
   1.992 +     */
   1.993 +    _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) {
   1.994 +        var notificationText;
   1.995 +        if (aOldLogin.username) {
   1.996 +            var displayUser = this._sanitizeUsername(aOldLogin.username);
   1.997 +            notificationText  = this._getLocalizedString(
   1.998 +                                          "updatePasswordMsg",
   1.999 +                                          [displayUser]);
  1.1000 +        } else {
  1.1001 +            notificationText  = this._getLocalizedString(
  1.1002 +                                          "updatePasswordMsgNoUser");
  1.1003 +        }
  1.1004 +
  1.1005 +        var changeButtonText =
  1.1006 +              this._getLocalizedString("notifyBarUpdateButtonText");
  1.1007 +        var changeButtonAccessKey =
  1.1008 +              this._getLocalizedString("notifyBarUpdateButtonAccessKey");
  1.1009 +
  1.1010 +        // The callbacks in |buttons| have a closure to access the variables
  1.1011 +        // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
  1.1012 +        // without a getService() call.
  1.1013 +        var self = this;
  1.1014 +
  1.1015 +        // Notification is a PopupNotification
  1.1016 +        if (aNotifyObj == this._getPopupNote()) {
  1.1017 +            // "Yes" button
  1.1018 +            var mainAction = {
  1.1019 +                label:     changeButtonText,
  1.1020 +                accessKey: changeButtonAccessKey,
  1.1021 +                popup:     null,
  1.1022 +                callback:  function(aNotifyObj, aButton) {
  1.1023 +                    self._updateLogin(aOldLogin, aNewPassword);
  1.1024 +                }
  1.1025 +            };
  1.1026 +
  1.1027 +            var notifyWin = this._getNotifyWindow();
  1.1028 +            var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
  1.1029 +            var browser = chromeWin.gBrowser.
  1.1030 +                                    getBrowserForDocument(notifyWin.top.document);
  1.1031 +
  1.1032 +            aNotifyObj.show(browser, "password-change", notificationText,
  1.1033 +                            "password-notification-icon", mainAction,
  1.1034 +                            null, { timeout: Date.now() + 10000,
  1.1035 +                                    persistWhileVisible: true });
  1.1036 +        } else {
  1.1037 +            var dontChangeButtonText =
  1.1038 +                  this._getLocalizedString("notifyBarDontChangeButtonText");
  1.1039 +            var dontChangeButtonAccessKey =
  1.1040 +                  this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
  1.1041 +            var buttons = [
  1.1042 +                // "Yes" button
  1.1043 +                {
  1.1044 +                    label:     changeButtonText,
  1.1045 +                    accessKey: changeButtonAccessKey,
  1.1046 +                    popup:     null,
  1.1047 +                    callback:  function(aNotifyObj, aButton) {
  1.1048 +                        self._updateLogin(aOldLogin, aNewPassword);
  1.1049 +                    }
  1.1050 +                },
  1.1051 +
  1.1052 +                // "No" button
  1.1053 +                {
  1.1054 +                    label:     dontChangeButtonText,
  1.1055 +                    accessKey: dontChangeButtonAccessKey,
  1.1056 +                    popup:     null,
  1.1057 +                    callback:  function(aNotifyObj, aButton) {
  1.1058 +                        // do nothing
  1.1059 +                    }
  1.1060 +                }
  1.1061 +            ];
  1.1062 +
  1.1063 +            this._showLoginNotification(aNotifyObj, "password-change",
  1.1064 +                                        notificationText, buttons);
  1.1065 +        }
  1.1066 +    },
  1.1067 +
  1.1068 +
  1.1069 +    /*
  1.1070 +     * _showChangeLoginDialog
  1.1071 +     *
  1.1072 +     * Shows the Change Password dialog.
  1.1073 +     *
  1.1074 +     */
  1.1075 +    _showChangeLoginDialog : function (aOldLogin, aNewPassword) {
  1.1076 +        const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  1.1077 +
  1.1078 +        var dialogText;
  1.1079 +        if (aOldLogin.username)
  1.1080 +            dialogText  = this._getLocalizedString(
  1.1081 +                                    "updatePasswordMsg",
  1.1082 +                                    [aOldLogin.username]);
  1.1083 +        else
  1.1084 +            dialogText  = this._getLocalizedString(
  1.1085 +                                    "updatePasswordMsgNoUser");
  1.1086 +
  1.1087 +        var dialogTitle = this._getLocalizedString(
  1.1088 +                                    "passwordChangeTitle");
  1.1089 +
  1.1090 +        // returns 0 for yes, 1 for no.
  1.1091 +        var ok = !this._promptService.confirmEx(this._window,
  1.1092 +                                dialogTitle, dialogText, buttonFlags,
  1.1093 +                                null, null, null,
  1.1094 +                                null, {});
  1.1095 +        if (ok) {
  1.1096 +            this.log("Updating password for user " + aOldLogin.username);
  1.1097 +            this._updateLogin(aOldLogin, aNewPassword);
  1.1098 +        }
  1.1099 +    },
  1.1100 +
  1.1101 +
  1.1102 +    /*
  1.1103 +     * promptToChangePasswordWithUsernames
  1.1104 +     *
  1.1105 +     * Called when we detect a password change in a form submission, but we
  1.1106 +     * don't know which existing login (username) it's for. Asks the user
  1.1107 +     * to select a username and confirm the password change.
  1.1108 +     *
  1.1109 +     * Note: The caller doesn't know the username for aNewLogin, so this
  1.1110 +     *       function fills in .username and .usernameField with the values
  1.1111 +     *       from the login selected by the user.
  1.1112 +     *
  1.1113 +     * Note; XPCOM stupidity: |count| is just |logins.length|.
  1.1114 +     */
  1.1115 +    promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
  1.1116 +        const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
  1.1117 +
  1.1118 +        var usernames = logins.map(function (l) l.username);
  1.1119 +        var dialogText  = this._getLocalizedString("userSelectText");
  1.1120 +        var dialogTitle = this._getLocalizedString("passwordChangeTitle");
  1.1121 +        var selectedIndex = { value: null };
  1.1122 +
  1.1123 +        // If user selects ok, outparam.value is set to the index
  1.1124 +        // of the selected username.
  1.1125 +        var ok = this._promptService.select(this._window,
  1.1126 +                                dialogTitle, dialogText,
  1.1127 +                                usernames.length, usernames,
  1.1128 +                                selectedIndex);
  1.1129 +        if (ok) {
  1.1130 +            // Now that we know which login to use, modify its password.
  1.1131 +            var selectedLogin = logins[selectedIndex.value];
  1.1132 +            this.log("Updating password for user " + selectedLogin.username);
  1.1133 +            this._updateLogin(selectedLogin, aNewLogin.password);
  1.1134 +        }
  1.1135 +    },
  1.1136 +
  1.1137 +
  1.1138 +
  1.1139 +
  1.1140 +    /* ---------- Internal Methods ---------- */
  1.1141 +
  1.1142 +
  1.1143 +
  1.1144 +
  1.1145 +    /*
  1.1146 +     * _updateLogin
  1.1147 +     */
  1.1148 +    _updateLogin : function (login, newPassword) {
  1.1149 +        var now = Date.now();
  1.1150 +        var propBag = Cc["@mozilla.org/hash-property-bag;1"].
  1.1151 +                      createInstance(Ci.nsIWritablePropertyBag);
  1.1152 +        if (newPassword) {
  1.1153 +            propBag.setProperty("password", newPassword);
  1.1154 +            // Explicitly set the password change time here (even though it would
  1.1155 +            // be changed automatically), to ensure that it's exactly the same
  1.1156 +            // value as timeLastUsed.
  1.1157 +            propBag.setProperty("timePasswordChanged", now);
  1.1158 +        }
  1.1159 +        propBag.setProperty("timeLastUsed", now);
  1.1160 +        propBag.setProperty("timesUsedIncrement", 1);
  1.1161 +        this._pwmgr.modifyLogin(login, propBag);
  1.1162 +    },
  1.1163 +
  1.1164 +
  1.1165 +    /*
  1.1166 +     * _getChromeWindow
  1.1167 +     *
  1.1168 +     * Given a content DOM window, returns the chrome window it's in.
  1.1169 +     */
  1.1170 +    _getChromeWindow: function (aWindow) {
  1.1171 +        var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1172 +                               .getInterface(Ci.nsIWebNavigation)
  1.1173 +                               .QueryInterface(Ci.nsIDocShell)
  1.1174 +                               .chromeEventHandler.ownerDocument.defaultView;
  1.1175 +        return chromeWin;
  1.1176 +    },
  1.1177 +
  1.1178 +
  1.1179 +    /*
  1.1180 +     * _getNotifyWindow
  1.1181 +     */
  1.1182 +    _getNotifyWindow: function () {
  1.1183 +
  1.1184 +        try {
  1.1185 +            // Get topmost window, in case we're in a frame.
  1.1186 +            var notifyWin = this._window.top;
  1.1187 +
  1.1188 +            // Some sites pop up a temporary login window, when disappears
  1.1189 +            // upon submission of credentials. We want to put the notification
  1.1190 +            // bar in the opener window if this seems to be happening.
  1.1191 +            if (notifyWin.opener) {
  1.1192 +                var chromeDoc = this._getChromeWindow(notifyWin).
  1.1193 +                                     document.documentElement;
  1.1194 +                var webnav = notifyWin.
  1.1195 +                             QueryInterface(Ci.nsIInterfaceRequestor).
  1.1196 +                             getInterface(Ci.nsIWebNavigation);
  1.1197 +
  1.1198 +                // Check to see if the current window was opened with chrome
  1.1199 +                // disabled, and if so use the opener window. But if the window
  1.1200 +                // has been used to visit other pages (ie, has a history),
  1.1201 +                // assume it'll stick around and *don't* use the opener.
  1.1202 +                if (chromeDoc.getAttribute("chromehidden") &&
  1.1203 +                    webnav.sessionHistory.count == 1) {
  1.1204 +                    this.log("Using opener window for notification bar.");
  1.1205 +                    notifyWin = notifyWin.opener;
  1.1206 +                }
  1.1207 +            }
  1.1208 +
  1.1209 +            return notifyWin;
  1.1210 +
  1.1211 +        } catch (e) {
  1.1212 +            // If any errors happen, just assume no notification box.
  1.1213 +            this.log("Unable to get notify window");
  1.1214 +            return null;
  1.1215 +        }
  1.1216 +    },
  1.1217 +
  1.1218 +
  1.1219 +    /*
  1.1220 +     * _getPopupNote
  1.1221 +     *
  1.1222 +     * Returns the popup notification to this prompter,
  1.1223 +     * or null if there isn't one available.
  1.1224 +     */
  1.1225 +    _getPopupNote : function () {
  1.1226 +        let popupNote = null;
  1.1227 +
  1.1228 +        try {
  1.1229 +            let notifyWin = this._getNotifyWindow();
  1.1230 +
  1.1231 +            // Get the chrome window for the content window we're using.
  1.1232 +            // .wrappedJSObject needed here -- see bug 422974 comment 5.
  1.1233 +            let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
  1.1234 +
  1.1235 +            popupNote = chromeWin.PopupNotifications;
  1.1236 +        } catch (e) {
  1.1237 +            this.log("Popup notifications not available on window");
  1.1238 +        }
  1.1239 +
  1.1240 +        return popupNote;
  1.1241 +    },
  1.1242 +
  1.1243 +
  1.1244 +    /*
  1.1245 +     * _getNotifyBox
  1.1246 +     *
  1.1247 +     * Returns the notification box to this prompter, or null if there isn't
  1.1248 +     * a notification box available.
  1.1249 +     */
  1.1250 +    _getNotifyBox : function () {
  1.1251 +        let notifyBox = null;
  1.1252 +
  1.1253 +        try {
  1.1254 +            let notifyWin = this._getNotifyWindow();
  1.1255 +
  1.1256 +            // Get the chrome window for the content window we're using.
  1.1257 +            // .wrappedJSObject needed here -- see bug 422974 comment 5.
  1.1258 +            let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
  1.1259 +
  1.1260 +            notifyBox = chromeWin.getNotificationBox(notifyWin);
  1.1261 +        } catch (e) {
  1.1262 +            this.log("Notification bars not available on window");
  1.1263 +        }
  1.1264 +
  1.1265 +        return notifyBox;
  1.1266 +    },
  1.1267 +
  1.1268 +
  1.1269 +    /*
  1.1270 +     * _repickSelectedLogin
  1.1271 +     *
  1.1272 +     * The user might enter a login that isn't the one we prefilled, but
  1.1273 +     * is the same as some other existing login. So, pick a login with a
  1.1274 +     * matching username, or return null.
  1.1275 +     */
  1.1276 +    _repickSelectedLogin : function (foundLogins, username) {
  1.1277 +        for (var i = 0; i < foundLogins.length; i++)
  1.1278 +            if (foundLogins[i].username == username)
  1.1279 +                return foundLogins[i];
  1.1280 +        return null;
  1.1281 +    },
  1.1282 +
  1.1283 +
  1.1284 +    /*
  1.1285 +     * _getLocalizedString
  1.1286 +     *
  1.1287 +     * Can be called as:
  1.1288 +     *   _getLocalizedString("key1");
  1.1289 +     *   _getLocalizedString("key2", ["arg1"]);
  1.1290 +     *   _getLocalizedString("key3", ["arg1", "arg2"]);
  1.1291 +     *   (etc)
  1.1292 +     *
  1.1293 +     * Returns the localized string for the specified key,
  1.1294 +     * formatted if required.
  1.1295 +     *
  1.1296 +     */
  1.1297 +    _getLocalizedString : function (key, formatArgs) {
  1.1298 +        if (formatArgs)
  1.1299 +            return this._strBundle.formatStringFromName(
  1.1300 +                                        key, formatArgs, formatArgs.length);
  1.1301 +        else
  1.1302 +            return this._strBundle.GetStringFromName(key);
  1.1303 +    },
  1.1304 +
  1.1305 +
  1.1306 +    /*
  1.1307 +     * _sanitizeUsername
  1.1308 +     *
  1.1309 +     * Sanitizes the specified username, by stripping quotes and truncating if
  1.1310 +     * it's too long. This helps prevent an evil site from messing with the
  1.1311 +     * "save password?" prompt too much.
  1.1312 +     */
  1.1313 +    _sanitizeUsername : function (username) {
  1.1314 +        if (username.length > 30) {
  1.1315 +            username = username.substring(0, 30);
  1.1316 +            username += this._ellipsis;
  1.1317 +        }
  1.1318 +        return username.replace(/['"]/g, "");
  1.1319 +    },
  1.1320 +
  1.1321 +
  1.1322 +    /*
  1.1323 +     * _getFormattedHostname
  1.1324 +     *
  1.1325 +     * The aURI parameter may either be a string uri, or an nsIURI instance.
  1.1326 +     *
  1.1327 +     * Returns the hostname to use in a nsILoginInfo object (for example,
  1.1328 +     * "http://example.com").
  1.1329 +     */
  1.1330 +    _getFormattedHostname : function (aURI) {
  1.1331 +        var uri;
  1.1332 +        if (aURI instanceof Ci.nsIURI) {
  1.1333 +            uri = aURI;
  1.1334 +        } else {
  1.1335 +            uri = Services.io.newURI(aURI, null, null);
  1.1336 +        }
  1.1337 +        var scheme = uri.scheme;
  1.1338 +
  1.1339 +        var hostname = scheme + "://" + uri.host;
  1.1340 +
  1.1341 +        // If the URI explicitly specified a port, only include it when
  1.1342 +        // it's not the default. (We never want "http://foo.com:80")
  1.1343 +        port = uri.port;
  1.1344 +        if (port != -1) {
  1.1345 +            var handler = Services.io.getProtocolHandler(scheme);
  1.1346 +            if (port != handler.defaultPort)
  1.1347 +                hostname += ":" + port;
  1.1348 +        }
  1.1349 +
  1.1350 +        return hostname;
  1.1351 +    },
  1.1352 +
  1.1353 +
  1.1354 +    /*
  1.1355 +     * _getShortDisplayHost
  1.1356 +     *
  1.1357 +     * Converts a login's hostname field (a URL) to a short string for
  1.1358 +     * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
  1.1359 +     * "ftp://www.site.co.uk" --> "site.co.uk".
  1.1360 +     */
  1.1361 +    _getShortDisplayHost: function (aURIString) {
  1.1362 +        var displayHost;
  1.1363 +
  1.1364 +        var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
  1.1365 +                          getService(Ci.nsIEffectiveTLDService);
  1.1366 +        var idnService = Cc["@mozilla.org/network/idn-service;1"].
  1.1367 +                         getService(Ci.nsIIDNService);
  1.1368 +        try {
  1.1369 +            var uri = Services.io.newURI(aURIString, null, null);
  1.1370 +            var baseDomain = eTLDService.getBaseDomain(uri);
  1.1371 +            displayHost = idnService.convertToDisplayIDN(baseDomain, {});
  1.1372 +        } catch (e) {
  1.1373 +            this.log("_getShortDisplayHost couldn't process " + aURIString);
  1.1374 +        }
  1.1375 +
  1.1376 +        if (!displayHost)
  1.1377 +            displayHost = aURIString;
  1.1378 +
  1.1379 +        return displayHost;
  1.1380 +    },
  1.1381 +
  1.1382 +
  1.1383 +    /*
  1.1384 +     * _getAuthTarget
  1.1385 +     *
  1.1386 +     * Returns the hostname and realm for which authentication is being
  1.1387 +     * requested, in the format expected to be used with nsILoginInfo.
  1.1388 +     */
  1.1389 +    _getAuthTarget : function (aChannel, aAuthInfo) {
  1.1390 +        var hostname, realm;
  1.1391 +
  1.1392 +        // If our proxy is demanding authentication, don't use the
  1.1393 +        // channel's actual destination.
  1.1394 +        if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
  1.1395 +            this.log("getAuthTarget is for proxy auth");
  1.1396 +            if (!(aChannel instanceof Ci.nsIProxiedChannel))
  1.1397 +                throw "proxy auth needs nsIProxiedChannel";
  1.1398 +
  1.1399 +            var info = aChannel.proxyInfo;
  1.1400 +            if (!info)
  1.1401 +                throw "proxy auth needs nsIProxyInfo";
  1.1402 +
  1.1403 +            // Proxies don't have a scheme, but we'll use "moz-proxy://"
  1.1404 +            // so that it's more obvious what the login is for.
  1.1405 +            var idnService = Cc["@mozilla.org/network/idn-service;1"].
  1.1406 +                             getService(Ci.nsIIDNService);
  1.1407 +            hostname = "moz-proxy://" +
  1.1408 +                        idnService.convertUTF8toACE(info.host) +
  1.1409 +                        ":" + info.port;
  1.1410 +            realm = aAuthInfo.realm;
  1.1411 +            if (!realm)
  1.1412 +                realm = hostname;
  1.1413 +
  1.1414 +            return [hostname, realm];
  1.1415 +        }
  1.1416 +
  1.1417 +        hostname = this._getFormattedHostname(aChannel.URI);
  1.1418 +
  1.1419 +        // If a HTTP WWW-Authenticate header specified a realm, that value
  1.1420 +        // will be available here. If it wasn't set or wasn't HTTP, we'll use
  1.1421 +        // the formatted hostname instead.
  1.1422 +        realm = aAuthInfo.realm;
  1.1423 +        if (!realm)
  1.1424 +            realm = hostname;
  1.1425 +
  1.1426 +        return [hostname, realm];
  1.1427 +    },
  1.1428 +
  1.1429 +
  1.1430 +    /**
  1.1431 +     * Returns [username, password] as extracted from aAuthInfo (which
  1.1432 +     * holds this info after having prompted the user).
  1.1433 +     *
  1.1434 +     * If the authentication was for a Windows domain, we'll prepend the
  1.1435 +     * return username with the domain. (eg, "domain\user")
  1.1436 +     */
  1.1437 +    _GetAuthInfo : function (aAuthInfo) {
  1.1438 +        var username, password;
  1.1439 +
  1.1440 +        var flags = aAuthInfo.flags;
  1.1441 +        if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
  1.1442 +            username = aAuthInfo.domain + "\\" + aAuthInfo.username;
  1.1443 +        else
  1.1444 +            username = aAuthInfo.username;
  1.1445 +
  1.1446 +        password = aAuthInfo.password;
  1.1447 +
  1.1448 +        return [username, password];
  1.1449 +    },
  1.1450 +
  1.1451 +
  1.1452 +    /**
  1.1453 +     * Given a username (possibly in DOMAIN\user form) and password, parses the
  1.1454 +     * domain out of the username if necessary and sets domain, username and
  1.1455 +     * password on the auth information object.
  1.1456 +     */
  1.1457 +    _SetAuthInfo : function (aAuthInfo, username, password) {
  1.1458 +        var flags = aAuthInfo.flags;
  1.1459 +        if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
  1.1460 +            // Domain is separated from username by a backslash
  1.1461 +            var idx = username.indexOf("\\");
  1.1462 +            if (idx == -1) {
  1.1463 +                aAuthInfo.username = username;
  1.1464 +            } else {
  1.1465 +                aAuthInfo.domain   =  username.substring(0, idx);
  1.1466 +                aAuthInfo.username =  username.substring(idx+1);
  1.1467 +            }
  1.1468 +        } else {
  1.1469 +            aAuthInfo.username = username;
  1.1470 +        }
  1.1471 +        aAuthInfo.password = password;
  1.1472 +    },
  1.1473 +
  1.1474 +    _newAsyncPromptConsumer : function(aCallback, aContext) {
  1.1475 +        return {
  1.1476 +            QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
  1.1477 +            callback: aCallback,
  1.1478 +            context: aContext,
  1.1479 +            cancel: function() {
  1.1480 +                this.callback.onAuthCancelled(this.context, false);
  1.1481 +                this.callback = null;
  1.1482 +                this.context = null;
  1.1483 +            }
  1.1484 +        }
  1.1485 +    }
  1.1486 +
  1.1487 +}; // end of LoginManagerPrompter implementation
  1.1488 +
  1.1489 +
  1.1490 +var component = [LoginManagerPromptFactory, LoginManagerPrompter];
  1.1491 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial