toolkit/components/passwordmgr/nsLoginManagerPrompter.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5
michael@0 6 const Cc = Components.classes;
michael@0 7 const Ci = Components.interfaces;
michael@0 8 const Cr = Components.results;
michael@0 9
michael@0 10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 11 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 12 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 13
michael@0 14 /*
michael@0 15 * LoginManagerPromptFactory
michael@0 16 *
michael@0 17 * Implements nsIPromptFactory
michael@0 18 *
michael@0 19 * Invoked by [toolkit/components/prompts/src/nsPrompter.js]
michael@0 20 */
michael@0 21 function LoginManagerPromptFactory() {
michael@0 22 Services.obs.addObserver(this, "quit-application-granted", true);
michael@0 23 Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
michael@0 24 Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
michael@0 25 }
michael@0 26
michael@0 27 LoginManagerPromptFactory.prototype = {
michael@0 28
michael@0 29 classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
michael@0 30 QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
michael@0 31
michael@0 32 _debug : false,
michael@0 33 _asyncPrompts : {},
michael@0 34 _asyncPromptInProgress : false,
michael@0 35
michael@0 36 observe : function (subject, topic, data) {
michael@0 37 this.log("Observed: " + topic);
michael@0 38 if (topic == "quit-application-granted") {
michael@0 39 this._cancelPendingPrompts();
michael@0 40 } else if (topic == "passwordmgr-crypto-login") {
michael@0 41 // Start processing the deferred prompters.
michael@0 42 this._doAsyncPrompt();
michael@0 43 } else if (topic == "passwordmgr-crypto-loginCanceled") {
michael@0 44 // User canceled a Master Password prompt, so go ahead and cancel
michael@0 45 // all pending auth prompts to avoid nagging over and over.
michael@0 46 this._cancelPendingPrompts();
michael@0 47 }
michael@0 48 },
michael@0 49
michael@0 50 getPrompt : function (aWindow, aIID) {
michael@0 51 var prefBranch = Services.prefs.getBranch("signon.");
michael@0 52 this._debug = prefBranch.getBoolPref("debug");
michael@0 53
michael@0 54 var prompt = new LoginManagerPrompter().QueryInterface(aIID);
michael@0 55 prompt.init(aWindow, this);
michael@0 56 return prompt;
michael@0 57 },
michael@0 58
michael@0 59 _doAsyncPrompt : function() {
michael@0 60 if (this._asyncPromptInProgress) {
michael@0 61 this.log("_doAsyncPrompt bypassed, already in progress");
michael@0 62 return;
michael@0 63 }
michael@0 64
michael@0 65 // Find the first prompt key we have in the queue
michael@0 66 var hashKey = null;
michael@0 67 for (hashKey in this._asyncPrompts)
michael@0 68 break;
michael@0 69
michael@0 70 if (!hashKey) {
michael@0 71 this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
michael@0 72 return;
michael@0 73 }
michael@0 74
michael@0 75 // If login manger has logins for this host, defer prompting if we're
michael@0 76 // already waiting on a master password entry.
michael@0 77 var prompt = this._asyncPrompts[hashKey];
michael@0 78 var prompter = prompt.prompter;
michael@0 79 var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo);
michael@0 80 var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0);
michael@0 81 if (hasLogins && prompter._pwmgr.uiBusy) {
michael@0 82 this.log("_doAsyncPrompt:run bypassed, master password UI busy");
michael@0 83 return;
michael@0 84 }
michael@0 85
michael@0 86 this._asyncPromptInProgress = true;
michael@0 87 prompt.inProgress = true;
michael@0 88
michael@0 89 var self = this;
michael@0 90
michael@0 91 var runnable = {
michael@0 92 run : function() {
michael@0 93 var ok = false;
michael@0 94 try {
michael@0 95 self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
michael@0 96 ok = prompter.promptAuth(prompt.channel,
michael@0 97 prompt.level,
michael@0 98 prompt.authInfo);
michael@0 99 } catch (e if (e instanceof Components.Exception) &&
michael@0 100 e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
michael@0 101 self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
michael@0 102 } catch (e) {
michael@0 103 Components.utils.reportError("LoginManagerPrompter: " +
michael@0 104 "_doAsyncPrompt:run: " + e + "\n");
michael@0 105 }
michael@0 106
michael@0 107 delete self._asyncPrompts[hashKey];
michael@0 108 prompt.inProgress = false;
michael@0 109 self._asyncPromptInProgress = false;
michael@0 110
michael@0 111 for each (var consumer in prompt.consumers) {
michael@0 112 if (!consumer.callback)
michael@0 113 // Not having a callback means that consumer didn't provide it
michael@0 114 // or canceled the notification
michael@0 115 continue;
michael@0 116
michael@0 117 self.log("Calling back to " + consumer.callback + " ok=" + ok);
michael@0 118 try {
michael@0 119 if (ok)
michael@0 120 consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
michael@0 121 else
michael@0 122 consumer.callback.onAuthCancelled(consumer.context, true);
michael@0 123 } catch (e) { /* Throw away exceptions caused by callback */ }
michael@0 124 }
michael@0 125 self._doAsyncPrompt();
michael@0 126 }
michael@0 127 }
michael@0 128
michael@0 129 Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 130 this.log("_doAsyncPrompt:run dispatched");
michael@0 131 },
michael@0 132
michael@0 133
michael@0 134 _cancelPendingPrompts : function() {
michael@0 135 this.log("Canceling all pending prompts...");
michael@0 136 var asyncPrompts = this._asyncPrompts;
michael@0 137 this.__proto__._asyncPrompts = {};
michael@0 138
michael@0 139 for each (var prompt in asyncPrompts) {
michael@0 140 // Watch out! If this prompt is currently prompting, let it handle
michael@0 141 // notifying the callbacks of success/failure, since it's already
michael@0 142 // asking the user for input. Reusing a callback can be crashy.
michael@0 143 if (prompt.inProgress) {
michael@0 144 this.log("skipping a prompt in progress");
michael@0 145 continue;
michael@0 146 }
michael@0 147
michael@0 148 for each (var consumer in prompt.consumers) {
michael@0 149 if (!consumer.callback)
michael@0 150 continue;
michael@0 151
michael@0 152 this.log("Canceling async auth prompt callback " + consumer.callback);
michael@0 153 try {
michael@0 154 consumer.callback.onAuthCancelled(consumer.context, true);
michael@0 155 } catch (e) { /* Just ignore exceptions from the callback */ }
michael@0 156 }
michael@0 157 }
michael@0 158 },
michael@0 159
michael@0 160
michael@0 161 log : function (message) {
michael@0 162 if (!this._debug)
michael@0 163 return;
michael@0 164
michael@0 165 dump("Pwmgr PromptFactory: " + message + "\n");
michael@0 166 Services.console.logStringMessage("Pwmgr PrompFactory: " + message);
michael@0 167 }
michael@0 168 }; // end of LoginManagerPromptFactory implementation
michael@0 169
michael@0 170
michael@0 171
michael@0 172
michael@0 173 /* ==================== LoginManagerPrompter ==================== */
michael@0 174
michael@0 175
michael@0 176
michael@0 177
michael@0 178 /*
michael@0 179 * LoginManagerPrompter
michael@0 180 *
michael@0 181 * Implements interfaces for prompting the user to enter/save/change auth info.
michael@0 182 *
michael@0 183 * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
michael@0 184 *
michael@0 185 * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
michael@0 186 * (eg HTTP Authenticate, FTP login).
michael@0 187 *
michael@0 188 * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
michael@0 189 * found in HTML forms.
michael@0 190 */
michael@0 191 function LoginManagerPrompter() {}
michael@0 192
michael@0 193 LoginManagerPrompter.prototype = {
michael@0 194
michael@0 195 classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
michael@0 196 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
michael@0 197 Ci.nsIAuthPrompt2,
michael@0 198 Ci.nsILoginManagerPrompter]),
michael@0 199
michael@0 200 _factory : null,
michael@0 201 _window : null,
michael@0 202 _debug : false, // mirrors signon.debug
michael@0 203
michael@0 204 __pwmgr : null, // Password Manager service
michael@0 205 get _pwmgr() {
michael@0 206 if (!this.__pwmgr)
michael@0 207 this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
michael@0 208 getService(Ci.nsILoginManager);
michael@0 209 return this.__pwmgr;
michael@0 210 },
michael@0 211
michael@0 212 __promptService : null, // Prompt service for user interaction
michael@0 213 get _promptService() {
michael@0 214 if (!this.__promptService)
michael@0 215 this.__promptService =
michael@0 216 Cc["@mozilla.org/embedcomp/prompt-service;1"].
michael@0 217 getService(Ci.nsIPromptService2);
michael@0 218 return this.__promptService;
michael@0 219 },
michael@0 220
michael@0 221
michael@0 222 __strBundle : null, // String bundle for L10N
michael@0 223 get _strBundle() {
michael@0 224 if (!this.__strBundle) {
michael@0 225 var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
michael@0 226 getService(Ci.nsIStringBundleService);
michael@0 227 this.__strBundle = bunService.createBundle(
michael@0 228 "chrome://passwordmgr/locale/passwordmgr.properties");
michael@0 229 if (!this.__strBundle)
michael@0 230 throw "String bundle for Login Manager not present!";
michael@0 231 }
michael@0 232
michael@0 233 return this.__strBundle;
michael@0 234 },
michael@0 235
michael@0 236
michael@0 237 __ellipsis : null,
michael@0 238 get _ellipsis() {
michael@0 239 if (!this.__ellipsis) {
michael@0 240 this.__ellipsis = "\u2026";
michael@0 241 try {
michael@0 242 this.__ellipsis = Services.prefs.getComplexValue(
michael@0 243 "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
michael@0 244 } catch (e) { }
michael@0 245 }
michael@0 246 return this.__ellipsis;
michael@0 247 },
michael@0 248
michael@0 249
michael@0 250 // Whether we are in private browsing mode
michael@0 251 get _inPrivateBrowsing() {
michael@0 252 if (this._window) {
michael@0 253 return PrivateBrowsingUtils.isWindowPrivate(this._window);
michael@0 254 } else {
michael@0 255 // If we don't that we're in private browsing mode if the caller did
michael@0 256 // not provide a window. The callers which really care about this
michael@0 257 // will indeed pass down a window to us, and for those who don't,
michael@0 258 // we can just assume that we don't want to save the entered login
michael@0 259 // information.
michael@0 260 return true;
michael@0 261 }
michael@0 262 },
michael@0 263
michael@0 264
michael@0 265 /*
michael@0 266 * log
michael@0 267 *
michael@0 268 * Internal function for logging debug messages to the Error Console window.
michael@0 269 */
michael@0 270 log : function (message) {
michael@0 271 if (!this._debug)
michael@0 272 return;
michael@0 273
michael@0 274 dump("Pwmgr Prompter: " + message + "\n");
michael@0 275 Services.console.logStringMessage("Pwmgr Prompter: " + message);
michael@0 276 },
michael@0 277
michael@0 278
michael@0 279
michael@0 280
michael@0 281 /* ---------- nsIAuthPrompt prompts ---------- */
michael@0 282
michael@0 283
michael@0 284 /*
michael@0 285 * prompt
michael@0 286 *
michael@0 287 * Wrapper around the prompt service prompt. Saving random fields here
michael@0 288 * doesn't really make sense and therefore isn't implemented.
michael@0 289 */
michael@0 290 prompt : function (aDialogTitle, aText, aPasswordRealm,
michael@0 291 aSavePassword, aDefaultText, aResult) {
michael@0 292 if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
michael@0 293 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 294
michael@0 295 this.log("===== prompt() called =====");
michael@0 296
michael@0 297 if (aDefaultText) {
michael@0 298 aResult.value = aDefaultText;
michael@0 299 }
michael@0 300
michael@0 301 return this._promptService.prompt(this._window,
michael@0 302 aDialogTitle, aText, aResult, null, {});
michael@0 303 },
michael@0 304
michael@0 305
michael@0 306 /*
michael@0 307 * promptUsernameAndPassword
michael@0 308 *
michael@0 309 * Looks up a username and password in the database. Will prompt the user
michael@0 310 * with a dialog, even if a username and password are found.
michael@0 311 */
michael@0 312 promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
michael@0 313 aSavePassword, aUsername, aPassword) {
michael@0 314 this.log("===== promptUsernameAndPassword() called =====");
michael@0 315
michael@0 316 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
michael@0 317 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 318
michael@0 319 var selectedLogin = null;
michael@0 320 var checkBox = { value : false };
michael@0 321 var checkBoxLabel = null;
michael@0 322 var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
michael@0 323
michael@0 324 // If hostname is null, we can't save this login.
michael@0 325 if (hostname) {
michael@0 326 var canRememberLogin;
michael@0 327 if (this._inPrivateBrowsing)
michael@0 328 canRememberLogin = false;
michael@0 329 else
michael@0 330 canRememberLogin = (aSavePassword ==
michael@0 331 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
michael@0 332 this._pwmgr.getLoginSavingEnabled(hostname);
michael@0 333
michael@0 334 // if checkBoxLabel is null, the checkbox won't be shown at all.
michael@0 335 if (canRememberLogin)
michael@0 336 checkBoxLabel = this._getLocalizedString("rememberPassword");
michael@0 337
michael@0 338 // Look for existing logins.
michael@0 339 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
michael@0 340 realm);
michael@0 341
michael@0 342 // XXX Like the original code, we can't deal with multiple
michael@0 343 // account selection. (bug 227632)
michael@0 344 if (foundLogins.length > 0) {
michael@0 345 selectedLogin = foundLogins[0];
michael@0 346
michael@0 347 // If the caller provided a username, try to use it. If they
michael@0 348 // provided only a password, this will try to find a password-only
michael@0 349 // login (or return null if none exists).
michael@0 350 if (aUsername.value)
michael@0 351 selectedLogin = this._repickSelectedLogin(foundLogins,
michael@0 352 aUsername.value);
michael@0 353
michael@0 354 if (selectedLogin) {
michael@0 355 checkBox.value = true;
michael@0 356 aUsername.value = selectedLogin.username;
michael@0 357 // If the caller provided a password, prefer it.
michael@0 358 if (!aPassword.value)
michael@0 359 aPassword.value = selectedLogin.password;
michael@0 360 }
michael@0 361 }
michael@0 362 }
michael@0 363
michael@0 364 var ok = this._promptService.promptUsernameAndPassword(this._window,
michael@0 365 aDialogTitle, aText, aUsername, aPassword,
michael@0 366 checkBoxLabel, checkBox);
michael@0 367
michael@0 368 if (!ok || !checkBox.value || !hostname)
michael@0 369 return ok;
michael@0 370
michael@0 371 if (!aPassword.value) {
michael@0 372 this.log("No password entered, so won't offer to save.");
michael@0 373 return ok;
michael@0 374 }
michael@0 375
michael@0 376 // XXX We can't prompt with multiple logins yet (bug 227632), so
michael@0 377 // the entered login might correspond to an existing login
michael@0 378 // other than the one we originally selected.
michael@0 379 selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
michael@0 380
michael@0 381 // If we didn't find an existing login, or if the username
michael@0 382 // changed, save as a new login.
michael@0 383 if (!selectedLogin) {
michael@0 384 // add as new
michael@0 385 this.log("New login seen for " + realm);
michael@0 386 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
michael@0 387 createInstance(Ci.nsILoginInfo);
michael@0 388 newLogin.init(hostname, null, realm,
michael@0 389 aUsername.value, aPassword.value, "", "");
michael@0 390 this._pwmgr.addLogin(newLogin);
michael@0 391 } else if (aPassword.value != selectedLogin.password) {
michael@0 392 // update password
michael@0 393 this.log("Updating password for " + realm);
michael@0 394 this._updateLogin(selectedLogin, aPassword.value);
michael@0 395 } else {
michael@0 396 this.log("Login unchanged, no further action needed.");
michael@0 397 this._updateLogin(selectedLogin);
michael@0 398 }
michael@0 399
michael@0 400 return ok;
michael@0 401 },
michael@0 402
michael@0 403
michael@0 404 /*
michael@0 405 * promptPassword
michael@0 406 *
michael@0 407 * If a password is found in the database for the password realm, it is
michael@0 408 * returned straight away without displaying a dialog.
michael@0 409 *
michael@0 410 * If a password is not found in the database, the user will be prompted
michael@0 411 * with a dialog with a text field and ok/cancel buttons. If the user
michael@0 412 * allows it, then the password will be saved in the database.
michael@0 413 */
michael@0 414 promptPassword : function (aDialogTitle, aText, aPasswordRealm,
michael@0 415 aSavePassword, aPassword) {
michael@0 416 this.log("===== promptPassword called() =====");
michael@0 417
michael@0 418 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
michael@0 419 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 420
michael@0 421 var checkBox = { value : false };
michael@0 422 var checkBoxLabel = null;
michael@0 423 var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
michael@0 424
michael@0 425 username = decodeURIComponent(username);
michael@0 426
michael@0 427 // If hostname is null, we can't save this login.
michael@0 428 if (hostname && !this._inPrivateBrowsing) {
michael@0 429 var canRememberLogin = (aSavePassword ==
michael@0 430 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
michael@0 431 this._pwmgr.getLoginSavingEnabled(hostname);
michael@0 432
michael@0 433 // if checkBoxLabel is null, the checkbox won't be shown at all.
michael@0 434 if (canRememberLogin)
michael@0 435 checkBoxLabel = this._getLocalizedString("rememberPassword");
michael@0 436
michael@0 437 if (!aPassword.value) {
michael@0 438 // Look for existing logins.
michael@0 439 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
michael@0 440 realm);
michael@0 441
michael@0 442 // XXX Like the original code, we can't deal with multiple
michael@0 443 // account selection (bug 227632). We can deal with finding the
michael@0 444 // account based on the supplied username - but in this case we'll
michael@0 445 // just return the first match.
michael@0 446 for (var i = 0; i < foundLogins.length; ++i) {
michael@0 447 if (foundLogins[i].username == username) {
michael@0 448 aPassword.value = foundLogins[i].password;
michael@0 449 // wallet returned straight away, so this mimics that code
michael@0 450 return true;
michael@0 451 }
michael@0 452 }
michael@0 453 }
michael@0 454 }
michael@0 455
michael@0 456 var ok = this._promptService.promptPassword(this._window, aDialogTitle,
michael@0 457 aText, aPassword,
michael@0 458 checkBoxLabel, checkBox);
michael@0 459
michael@0 460 if (ok && checkBox.value && hostname && aPassword.value) {
michael@0 461 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
michael@0 462 createInstance(Ci.nsILoginInfo);
michael@0 463 newLogin.init(hostname, null, realm, username,
michael@0 464 aPassword.value, "", "");
michael@0 465
michael@0 466 this.log("New login seen for " + realm);
michael@0 467
michael@0 468 this._pwmgr.addLogin(newLogin);
michael@0 469 }
michael@0 470
michael@0 471 return ok;
michael@0 472 },
michael@0 473
michael@0 474 /* ---------- nsIAuthPrompt helpers ---------- */
michael@0 475
michael@0 476
michael@0 477 /**
michael@0 478 * Given aRealmString, such as "http://user@example.com/foo", returns an
michael@0 479 * array of:
michael@0 480 * - the formatted hostname
michael@0 481 * - the realm (hostname + path)
michael@0 482 * - the username, if present
michael@0 483 *
michael@0 484 * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
michael@0 485 * channels, e.g. "example.com:80 (httprealm)", null is returned for all
michael@0 486 * arguments to let callers know the login can't be saved because we don't
michael@0 487 * know whether it's http or https.
michael@0 488 */
michael@0 489 _getRealmInfo : function (aRealmString) {
michael@0 490 var httpRealm = /^.+ \(.+\)$/;
michael@0 491 if (httpRealm.test(aRealmString))
michael@0 492 return [null, null, null];
michael@0 493
michael@0 494 var uri = Services.io.newURI(aRealmString, null, null);
michael@0 495 var pathname = "";
michael@0 496
michael@0 497 if (uri.path != "/")
michael@0 498 pathname = uri.path;
michael@0 499
michael@0 500 var formattedHostname = this._getFormattedHostname(uri);
michael@0 501
michael@0 502 return [formattedHostname, formattedHostname + pathname, uri.username];
michael@0 503 },
michael@0 504
michael@0 505 /* ---------- nsIAuthPrompt2 prompts ---------- */
michael@0 506
michael@0 507
michael@0 508
michael@0 509
michael@0 510 /*
michael@0 511 * promptAuth
michael@0 512 *
michael@0 513 * Implementation of nsIAuthPrompt2.
michael@0 514 *
michael@0 515 * nsIChannel aChannel
michael@0 516 * int aLevel
michael@0 517 * nsIAuthInformation aAuthInfo
michael@0 518 */
michael@0 519 promptAuth : function (aChannel, aLevel, aAuthInfo) {
michael@0 520 var selectedLogin = null;
michael@0 521 var checkbox = { value : false };
michael@0 522 var checkboxLabel = null;
michael@0 523 var epicfail = false;
michael@0 524 var canAutologin = false;
michael@0 525
michael@0 526 try {
michael@0 527
michael@0 528 this.log("===== promptAuth called =====");
michael@0 529
michael@0 530 // If the user submits a login but it fails, we need to remove the
michael@0 531 // notification bar that was displayed. Conveniently, the user will
michael@0 532 // be prompted for authentication again, which brings us here.
michael@0 533 this._removeLoginNotifications();
michael@0 534
michael@0 535 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
michael@0 536
michael@0 537
michael@0 538 // Looks for existing logins to prefill the prompt with.
michael@0 539 var foundLogins = this._pwmgr.findLogins({},
michael@0 540 hostname, null, httpRealm);
michael@0 541 this.log("found " + foundLogins.length + " matching logins.");
michael@0 542
michael@0 543 // XXX Can't select from multiple accounts yet. (bug 227632)
michael@0 544 if (foundLogins.length > 0) {
michael@0 545 selectedLogin = foundLogins[0];
michael@0 546 this._SetAuthInfo(aAuthInfo, selectedLogin.username,
michael@0 547 selectedLogin.password);
michael@0 548
michael@0 549 // Allow automatic proxy login
michael@0 550 if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
michael@0 551 !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
michael@0 552 Services.prefs.getBoolPref("signon.autologin.proxy") &&
michael@0 553 !this._inPrivateBrowsing) {
michael@0 554
michael@0 555 this.log("Autologin enabled, skipping auth prompt.");
michael@0 556 canAutologin = true;
michael@0 557 }
michael@0 558
michael@0 559 checkbox.value = true;
michael@0 560 }
michael@0 561
michael@0 562 var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
michael@0 563 if (this._inPrivateBrowsing)
michael@0 564 canRememberLogin = false;
michael@0 565
michael@0 566 // if checkboxLabel is null, the checkbox won't be shown at all.
michael@0 567 var notifyBox = this._getNotifyBox();
michael@0 568 if (canRememberLogin && !notifyBox)
michael@0 569 checkboxLabel = this._getLocalizedString("rememberPassword");
michael@0 570 } catch (e) {
michael@0 571 // Ignore any errors and display the prompt anyway.
michael@0 572 epicfail = true;
michael@0 573 Components.utils.reportError("LoginManagerPrompter: " +
michael@0 574 "Epic fail in promptAuth: " + e + "\n");
michael@0 575 }
michael@0 576
michael@0 577 var ok = canAutologin ||
michael@0 578 this._promptService.promptAuth(this._window,
michael@0 579 aChannel, aLevel, aAuthInfo,
michael@0 580 checkboxLabel, checkbox);
michael@0 581
michael@0 582 // If there's a notification box, use it to allow the user to
michael@0 583 // determine if the login should be saved. If there isn't a
michael@0 584 // notification box, only save the login if the user set the
michael@0 585 // checkbox to do so.
michael@0 586 var rememberLogin = notifyBox ? canRememberLogin : checkbox.value;
michael@0 587 if (!ok || !rememberLogin || epicfail)
michael@0 588 return ok;
michael@0 589
michael@0 590 try {
michael@0 591 var [username, password] = this._GetAuthInfo(aAuthInfo);
michael@0 592
michael@0 593 if (!password) {
michael@0 594 this.log("No password entered, so won't offer to save.");
michael@0 595 return ok;
michael@0 596 }
michael@0 597
michael@0 598 // XXX We can't prompt with multiple logins yet (bug 227632), so
michael@0 599 // the entered login might correspond to an existing login
michael@0 600 // other than the one we originally selected.
michael@0 601 selectedLogin = this._repickSelectedLogin(foundLogins, username);
michael@0 602
michael@0 603 // If we didn't find an existing login, or if the username
michael@0 604 // changed, save as a new login.
michael@0 605 if (!selectedLogin) {
michael@0 606 this.log("New login seen for " + username +
michael@0 607 " @ " + hostname + " (" + httpRealm + ")");
michael@0 608
michael@0 609 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
michael@0 610 createInstance(Ci.nsILoginInfo);
michael@0 611 newLogin.init(hostname, null, httpRealm,
michael@0 612 username, password, "", "");
michael@0 613 var notifyObj = this._getPopupNote() || notifyBox;
michael@0 614 if (notifyObj)
michael@0 615 this._showSaveLoginNotification(notifyObj, newLogin);
michael@0 616 else
michael@0 617 this._pwmgr.addLogin(newLogin);
michael@0 618
michael@0 619 } else if (password != selectedLogin.password) {
michael@0 620
michael@0 621 this.log("Updating password for " + username +
michael@0 622 " @ " + hostname + " (" + httpRealm + ")");
michael@0 623 var notifyObj = this._getPopupNote() || notifyBox;
michael@0 624 if (notifyObj)
michael@0 625 this._showChangeLoginNotification(notifyObj,
michael@0 626 selectedLogin, password);
michael@0 627 else
michael@0 628 this._updateLogin(selectedLogin, password);
michael@0 629
michael@0 630 } else {
michael@0 631 this.log("Login unchanged, no further action needed.");
michael@0 632 this._updateLogin(selectedLogin);
michael@0 633 }
michael@0 634 } catch (e) {
michael@0 635 Components.utils.reportError("LoginManagerPrompter: " +
michael@0 636 "Fail2 in promptAuth: " + e + "\n");
michael@0 637 }
michael@0 638
michael@0 639 return ok;
michael@0 640 },
michael@0 641
michael@0 642 asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
michael@0 643 var cancelable = null;
michael@0 644
michael@0 645 try {
michael@0 646 this.log("===== asyncPromptAuth called =====");
michael@0 647
michael@0 648 // If the user submits a login but it fails, we need to remove the
michael@0 649 // notification bar that was displayed. Conveniently, the user will
michael@0 650 // be prompted for authentication again, which brings us here.
michael@0 651 this._removeLoginNotifications();
michael@0 652
michael@0 653 cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
michael@0 654
michael@0 655 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
michael@0 656
michael@0 657 var hashKey = aLevel + "|" + hostname + "|" + httpRealm;
michael@0 658 this.log("Async prompt key = " + hashKey);
michael@0 659 var asyncPrompt = this._factory._asyncPrompts[hashKey];
michael@0 660 if (asyncPrompt) {
michael@0 661 this.log("Prompt bound to an existing one in the queue, callback = " + aCallback);
michael@0 662 asyncPrompt.consumers.push(cancelable);
michael@0 663 return cancelable;
michael@0 664 }
michael@0 665
michael@0 666 this.log("Adding new prompt to the queue, callback = " + aCallback);
michael@0 667 asyncPrompt = {
michael@0 668 consumers: [cancelable],
michael@0 669 channel: aChannel,
michael@0 670 authInfo: aAuthInfo,
michael@0 671 level: aLevel,
michael@0 672 inProgress : false,
michael@0 673 prompter: this
michael@0 674 }
michael@0 675
michael@0 676 this._factory._asyncPrompts[hashKey] = asyncPrompt;
michael@0 677 this._factory._doAsyncPrompt();
michael@0 678 }
michael@0 679 catch (e) {
michael@0 680 Components.utils.reportError("LoginManagerPrompter: " +
michael@0 681 "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n");
michael@0 682 // Fail the prompt operation to let the consumer fall back
michael@0 683 // to synchronous promptAuth method
michael@0 684 throw e;
michael@0 685 }
michael@0 686
michael@0 687 return cancelable;
michael@0 688 },
michael@0 689
michael@0 690
michael@0 691
michael@0 692
michael@0 693 /* ---------- nsILoginManagerPrompter prompts ---------- */
michael@0 694
michael@0 695
michael@0 696
michael@0 697
michael@0 698 /*
michael@0 699 * init
michael@0 700 *
michael@0 701 */
michael@0 702 init : function (aWindow, aFactory) {
michael@0 703 this._window = aWindow;
michael@0 704 this._factory = aFactory || null;
michael@0 705
michael@0 706 var prefBranch = Services.prefs.getBranch("signon.");
michael@0 707 this._debug = prefBranch.getBoolPref("debug");
michael@0 708 this.log("===== initialized =====");
michael@0 709 },
michael@0 710
michael@0 711
michael@0 712 /*
michael@0 713 * promptToSavePassword
michael@0 714 *
michael@0 715 */
michael@0 716 promptToSavePassword : function (aLogin) {
michael@0 717 var notifyObj = this._getPopupNote() || this._getNotifyBox();
michael@0 718
michael@0 719 if (notifyObj)
michael@0 720 this._showSaveLoginNotification(notifyObj, aLogin);
michael@0 721 else
michael@0 722 this._showSaveLoginDialog(aLogin);
michael@0 723 },
michael@0 724
michael@0 725
michael@0 726 /*
michael@0 727 * _showLoginNotification
michael@0 728 *
michael@0 729 * Displays a notification bar.
michael@0 730 *
michael@0 731 */
michael@0 732 _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
michael@0 733 var oldBar = aNotifyBox.getNotificationWithValue(aName);
michael@0 734 const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
michael@0 735
michael@0 736 this.log("Adding new " + aName + " notification bar");
michael@0 737 var newBar = aNotifyBox.appendNotification(
michael@0 738 aText, aName,
michael@0 739 "chrome://mozapps/skin/passwordmgr/key.png",
michael@0 740 priority, aButtons);
michael@0 741
michael@0 742 // The page we're going to hasn't loaded yet, so we want to persist
michael@0 743 // across the first location change.
michael@0 744 newBar.persistence++;
michael@0 745
michael@0 746 // Sites like Gmail perform a funky redirect dance before you end up
michael@0 747 // at the post-authentication page. I don't see a good way to
michael@0 748 // heuristically determine when to ignore such location changes, so
michael@0 749 // we'll try ignoring location changes based on a time interval.
michael@0 750 newBar.timeout = Date.now() + 20000; // 20 seconds
michael@0 751
michael@0 752 if (oldBar) {
michael@0 753 this.log("(...and removing old " + aName + " notification bar)");
michael@0 754 aNotifyBox.removeNotification(oldBar);
michael@0 755 }
michael@0 756 },
michael@0 757
michael@0 758
michael@0 759 /*
michael@0 760 * _showSaveLoginNotification
michael@0 761 *
michael@0 762 * Displays a notification bar or a popup notification, to allow the user
michael@0 763 * to save the specified login. This allows the user to see the results of
michael@0 764 * their login, and only save a login which they know worked.
michael@0 765 *
michael@0 766 * @param aNotifyObj
michael@0 767 * A notification box or a popup notification.
michael@0 768 */
michael@0 769 _showSaveLoginNotification : function (aNotifyObj, aLogin) {
michael@0 770
michael@0 771 // Ugh. We can't use the strings from the popup window, because they
michael@0 772 // have the access key marked in the string (eg "Mo&zilla"), along
michael@0 773 // with some weird rules for handling access keys that do not occur
michael@0 774 // in the string, for L10N. See commonDialog.js's setLabelForNode().
michael@0 775 var neverButtonText =
michael@0 776 this._getLocalizedString("notifyBarNeverRememberButtonText");
michael@0 777 var neverButtonAccessKey =
michael@0 778 this._getLocalizedString("notifyBarNeverRememberButtonAccessKey");
michael@0 779 var rememberButtonText =
michael@0 780 this._getLocalizedString("notifyBarRememberPasswordButtonText");
michael@0 781 var rememberButtonAccessKey =
michael@0 782 this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
michael@0 783
michael@0 784 var displayHost = this._getShortDisplayHost(aLogin.hostname);
michael@0 785 var notificationText;
michael@0 786 if (aLogin.username) {
michael@0 787 var displayUser = this._sanitizeUsername(aLogin.username);
michael@0 788 notificationText = this._getLocalizedString(
michael@0 789 "rememberPasswordMsg",
michael@0 790 [displayUser, displayHost]);
michael@0 791 } else {
michael@0 792 notificationText = this._getLocalizedString(
michael@0 793 "rememberPasswordMsgNoUsername",
michael@0 794 [displayHost]);
michael@0 795 }
michael@0 796
michael@0 797 // The callbacks in |buttons| have a closure to access the variables
michael@0 798 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
michael@0 799 // without a getService() call.
michael@0 800 var pwmgr = this._pwmgr;
michael@0 801
michael@0 802 // Notification is a PopupNotification
michael@0 803 if (aNotifyObj == this._getPopupNote()) {
michael@0 804 // "Remember" button
michael@0 805 var mainAction = {
michael@0 806 label: rememberButtonText,
michael@0 807 accessKey: rememberButtonAccessKey,
michael@0 808 callback: function(aNotifyObj, aButton) {
michael@0 809 pwmgr.addLogin(aLogin);
michael@0 810 browser.focus();
michael@0 811 }
michael@0 812 };
michael@0 813
michael@0 814 var secondaryActions = [
michael@0 815 // "Never for this site" button
michael@0 816 {
michael@0 817 label: neverButtonText,
michael@0 818 accessKey: neverButtonAccessKey,
michael@0 819 callback: function(aNotifyObj, aButton) {
michael@0 820 pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
michael@0 821 browser.focus();
michael@0 822 }
michael@0 823 }
michael@0 824 ];
michael@0 825
michael@0 826 var notifyWin = this._getNotifyWindow();
michael@0 827 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
michael@0 828 var browser = chromeWin.gBrowser.
michael@0 829 getBrowserForDocument(notifyWin.top.document);
michael@0 830
michael@0 831 aNotifyObj.show(browser, "password-save", notificationText,
michael@0 832 "password-notification-icon", mainAction,
michael@0 833 secondaryActions, { timeout: Date.now() + 10000,
michael@0 834 persistWhileVisible: true });
michael@0 835 } else {
michael@0 836 var notNowButtonText =
michael@0 837 this._getLocalizedString("notifyBarNotNowButtonText");
michael@0 838 var notNowButtonAccessKey =
michael@0 839 this._getLocalizedString("notifyBarNotNowButtonAccessKey");
michael@0 840 var buttons = [
michael@0 841 // "Remember" button
michael@0 842 {
michael@0 843 label: rememberButtonText,
michael@0 844 accessKey: rememberButtonAccessKey,
michael@0 845 popup: null,
michael@0 846 callback: function(aNotifyObj, aButton) {
michael@0 847 pwmgr.addLogin(aLogin);
michael@0 848 }
michael@0 849 },
michael@0 850
michael@0 851 // "Never for this site" button
michael@0 852 {
michael@0 853 label: neverButtonText,
michael@0 854 accessKey: neverButtonAccessKey,
michael@0 855 popup: null,
michael@0 856 callback: function(aNotifyObj, aButton) {
michael@0 857 pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
michael@0 858 }
michael@0 859 },
michael@0 860
michael@0 861 // "Not now" button
michael@0 862 {
michael@0 863 label: notNowButtonText,
michael@0 864 accessKey: notNowButtonAccessKey,
michael@0 865 popup: null,
michael@0 866 callback: function() { /* NOP */ }
michael@0 867 }
michael@0 868 ];
michael@0 869
michael@0 870 this._showLoginNotification(aNotifyObj, "password-save",
michael@0 871 notificationText, buttons);
michael@0 872 }
michael@0 873 },
michael@0 874
michael@0 875
michael@0 876 /*
michael@0 877 * _removeLoginNotifications
michael@0 878 *
michael@0 879 */
michael@0 880 _removeLoginNotifications : function () {
michael@0 881 var popupNote = this._getPopupNote();
michael@0 882 if (popupNote)
michael@0 883 popupNote = popupNote.getNotification("password-save");
michael@0 884 if (popupNote)
michael@0 885 popupNote.remove();
michael@0 886
michael@0 887 var notifyBox = this._getNotifyBox();
michael@0 888 if (notifyBox) {
michael@0 889 var oldBar = notifyBox.getNotificationWithValue("password-save");
michael@0 890 if (oldBar) {
michael@0 891 this.log("Removing save-password notification bar.");
michael@0 892 notifyBox.removeNotification(oldBar);
michael@0 893 }
michael@0 894
michael@0 895 oldBar = notifyBox.getNotificationWithValue("password-change");
michael@0 896 if (oldBar) {
michael@0 897 this.log("Removing change-password notification bar.");
michael@0 898 notifyBox.removeNotification(oldBar);
michael@0 899 }
michael@0 900 }
michael@0 901 },
michael@0 902
michael@0 903
michael@0 904 /*
michael@0 905 * _showSaveLoginDialog
michael@0 906 *
michael@0 907 * Called when we detect a new login in a form submission,
michael@0 908 * asks the user what to do.
michael@0 909 *
michael@0 910 */
michael@0 911 _showSaveLoginDialog : function (aLogin) {
michael@0 912 const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
michael@0 913 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
michael@0 914 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
michael@0 915 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
michael@0 916
michael@0 917 var displayHost = this._getShortDisplayHost(aLogin.hostname);
michael@0 918
michael@0 919 var dialogText;
michael@0 920 if (aLogin.username) {
michael@0 921 var displayUser = this._sanitizeUsername(aLogin.username);
michael@0 922 dialogText = this._getLocalizedString(
michael@0 923 "rememberPasswordMsg",
michael@0 924 [displayUser, displayHost]);
michael@0 925 } else {
michael@0 926 dialogText = this._getLocalizedString(
michael@0 927 "rememberPasswordMsgNoUsername",
michael@0 928 [displayHost]);
michael@0 929
michael@0 930 }
michael@0 931 var dialogTitle = this._getLocalizedString(
michael@0 932 "savePasswordTitle");
michael@0 933 var neverButtonText = this._getLocalizedString(
michael@0 934 "neverForSiteButtonText");
michael@0 935 var rememberButtonText = this._getLocalizedString(
michael@0 936 "rememberButtonText");
michael@0 937 var notNowButtonText = this._getLocalizedString(
michael@0 938 "notNowButtonText");
michael@0 939
michael@0 940 this.log("Prompting user to save/ignore login");
michael@0 941 var userChoice = this._promptService.confirmEx(this._window,
michael@0 942 dialogTitle, dialogText,
michael@0 943 buttonFlags, rememberButtonText,
michael@0 944 notNowButtonText, neverButtonText,
michael@0 945 null, {});
michael@0 946 // Returns:
michael@0 947 // 0 - Save the login
michael@0 948 // 1 - Ignore the login this time
michael@0 949 // 2 - Never save logins for this site
michael@0 950 if (userChoice == 2) {
michael@0 951 this.log("Disabling " + aLogin.hostname + " logins by request.");
michael@0 952 this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
michael@0 953 } else if (userChoice == 0) {
michael@0 954 this.log("Saving login for " + aLogin.hostname);
michael@0 955 this._pwmgr.addLogin(aLogin);
michael@0 956 } else {
michael@0 957 // userChoice == 1 --> just ignore the login.
michael@0 958 this.log("Ignoring login.");
michael@0 959 }
michael@0 960 },
michael@0 961
michael@0 962
michael@0 963 /*
michael@0 964 * promptToChangePassword
michael@0 965 *
michael@0 966 * Called when we think we detect a password change for an existing
michael@0 967 * login, when the form being submitted contains multiple password
michael@0 968 * fields.
michael@0 969 *
michael@0 970 */
michael@0 971 promptToChangePassword : function (aOldLogin, aNewLogin) {
michael@0 972 var notifyObj = this._getPopupNote() || this._getNotifyBox();
michael@0 973
michael@0 974 if (notifyObj)
michael@0 975 this._showChangeLoginNotification(notifyObj, aOldLogin,
michael@0 976 aNewLogin.password);
michael@0 977 else
michael@0 978 this._showChangeLoginDialog(aOldLogin, aNewLogin.password);
michael@0 979 },
michael@0 980
michael@0 981
michael@0 982 /*
michael@0 983 * _showChangeLoginNotification
michael@0 984 *
michael@0 985 * Shows the Change Password notification bar or popup notification.
michael@0 986 *
michael@0 987 * @param aNotifyObj
michael@0 988 * A notification box or a popup notification.
michael@0 989 */
michael@0 990 _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) {
michael@0 991 var notificationText;
michael@0 992 if (aOldLogin.username) {
michael@0 993 var displayUser = this._sanitizeUsername(aOldLogin.username);
michael@0 994 notificationText = this._getLocalizedString(
michael@0 995 "updatePasswordMsg",
michael@0 996 [displayUser]);
michael@0 997 } else {
michael@0 998 notificationText = this._getLocalizedString(
michael@0 999 "updatePasswordMsgNoUser");
michael@0 1000 }
michael@0 1001
michael@0 1002 var changeButtonText =
michael@0 1003 this._getLocalizedString("notifyBarUpdateButtonText");
michael@0 1004 var changeButtonAccessKey =
michael@0 1005 this._getLocalizedString("notifyBarUpdateButtonAccessKey");
michael@0 1006
michael@0 1007 // The callbacks in |buttons| have a closure to access the variables
michael@0 1008 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
michael@0 1009 // without a getService() call.
michael@0 1010 var self = this;
michael@0 1011
michael@0 1012 // Notification is a PopupNotification
michael@0 1013 if (aNotifyObj == this._getPopupNote()) {
michael@0 1014 // "Yes" button
michael@0 1015 var mainAction = {
michael@0 1016 label: changeButtonText,
michael@0 1017 accessKey: changeButtonAccessKey,
michael@0 1018 popup: null,
michael@0 1019 callback: function(aNotifyObj, aButton) {
michael@0 1020 self._updateLogin(aOldLogin, aNewPassword);
michael@0 1021 }
michael@0 1022 };
michael@0 1023
michael@0 1024 var notifyWin = this._getNotifyWindow();
michael@0 1025 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
michael@0 1026 var browser = chromeWin.gBrowser.
michael@0 1027 getBrowserForDocument(notifyWin.top.document);
michael@0 1028
michael@0 1029 aNotifyObj.show(browser, "password-change", notificationText,
michael@0 1030 "password-notification-icon", mainAction,
michael@0 1031 null, { timeout: Date.now() + 10000,
michael@0 1032 persistWhileVisible: true });
michael@0 1033 } else {
michael@0 1034 var dontChangeButtonText =
michael@0 1035 this._getLocalizedString("notifyBarDontChangeButtonText");
michael@0 1036 var dontChangeButtonAccessKey =
michael@0 1037 this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
michael@0 1038 var buttons = [
michael@0 1039 // "Yes" button
michael@0 1040 {
michael@0 1041 label: changeButtonText,
michael@0 1042 accessKey: changeButtonAccessKey,
michael@0 1043 popup: null,
michael@0 1044 callback: function(aNotifyObj, aButton) {
michael@0 1045 self._updateLogin(aOldLogin, aNewPassword);
michael@0 1046 }
michael@0 1047 },
michael@0 1048
michael@0 1049 // "No" button
michael@0 1050 {
michael@0 1051 label: dontChangeButtonText,
michael@0 1052 accessKey: dontChangeButtonAccessKey,
michael@0 1053 popup: null,
michael@0 1054 callback: function(aNotifyObj, aButton) {
michael@0 1055 // do nothing
michael@0 1056 }
michael@0 1057 }
michael@0 1058 ];
michael@0 1059
michael@0 1060 this._showLoginNotification(aNotifyObj, "password-change",
michael@0 1061 notificationText, buttons);
michael@0 1062 }
michael@0 1063 },
michael@0 1064
michael@0 1065
michael@0 1066 /*
michael@0 1067 * _showChangeLoginDialog
michael@0 1068 *
michael@0 1069 * Shows the Change Password dialog.
michael@0 1070 *
michael@0 1071 */
michael@0 1072 _showChangeLoginDialog : function (aOldLogin, aNewPassword) {
michael@0 1073 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
michael@0 1074
michael@0 1075 var dialogText;
michael@0 1076 if (aOldLogin.username)
michael@0 1077 dialogText = this._getLocalizedString(
michael@0 1078 "updatePasswordMsg",
michael@0 1079 [aOldLogin.username]);
michael@0 1080 else
michael@0 1081 dialogText = this._getLocalizedString(
michael@0 1082 "updatePasswordMsgNoUser");
michael@0 1083
michael@0 1084 var dialogTitle = this._getLocalizedString(
michael@0 1085 "passwordChangeTitle");
michael@0 1086
michael@0 1087 // returns 0 for yes, 1 for no.
michael@0 1088 var ok = !this._promptService.confirmEx(this._window,
michael@0 1089 dialogTitle, dialogText, buttonFlags,
michael@0 1090 null, null, null,
michael@0 1091 null, {});
michael@0 1092 if (ok) {
michael@0 1093 this.log("Updating password for user " + aOldLogin.username);
michael@0 1094 this._updateLogin(aOldLogin, aNewPassword);
michael@0 1095 }
michael@0 1096 },
michael@0 1097
michael@0 1098
michael@0 1099 /*
michael@0 1100 * promptToChangePasswordWithUsernames
michael@0 1101 *
michael@0 1102 * Called when we detect a password change in a form submission, but we
michael@0 1103 * don't know which existing login (username) it's for. Asks the user
michael@0 1104 * to select a username and confirm the password change.
michael@0 1105 *
michael@0 1106 * Note: The caller doesn't know the username for aNewLogin, so this
michael@0 1107 * function fills in .username and .usernameField with the values
michael@0 1108 * from the login selected by the user.
michael@0 1109 *
michael@0 1110 * Note; XPCOM stupidity: |count| is just |logins.length|.
michael@0 1111 */
michael@0 1112 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
michael@0 1113 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
michael@0 1114
michael@0 1115 var usernames = logins.map(function (l) l.username);
michael@0 1116 var dialogText = this._getLocalizedString("userSelectText");
michael@0 1117 var dialogTitle = this._getLocalizedString("passwordChangeTitle");
michael@0 1118 var selectedIndex = { value: null };
michael@0 1119
michael@0 1120 // If user selects ok, outparam.value is set to the index
michael@0 1121 // of the selected username.
michael@0 1122 var ok = this._promptService.select(this._window,
michael@0 1123 dialogTitle, dialogText,
michael@0 1124 usernames.length, usernames,
michael@0 1125 selectedIndex);
michael@0 1126 if (ok) {
michael@0 1127 // Now that we know which login to use, modify its password.
michael@0 1128 var selectedLogin = logins[selectedIndex.value];
michael@0 1129 this.log("Updating password for user " + selectedLogin.username);
michael@0 1130 this._updateLogin(selectedLogin, aNewLogin.password);
michael@0 1131 }
michael@0 1132 },
michael@0 1133
michael@0 1134
michael@0 1135
michael@0 1136
michael@0 1137 /* ---------- Internal Methods ---------- */
michael@0 1138
michael@0 1139
michael@0 1140
michael@0 1141
michael@0 1142 /*
michael@0 1143 * _updateLogin
michael@0 1144 */
michael@0 1145 _updateLogin : function (login, newPassword) {
michael@0 1146 var now = Date.now();
michael@0 1147 var propBag = Cc["@mozilla.org/hash-property-bag;1"].
michael@0 1148 createInstance(Ci.nsIWritablePropertyBag);
michael@0 1149 if (newPassword) {
michael@0 1150 propBag.setProperty("password", newPassword);
michael@0 1151 // Explicitly set the password change time here (even though it would
michael@0 1152 // be changed automatically), to ensure that it's exactly the same
michael@0 1153 // value as timeLastUsed.
michael@0 1154 propBag.setProperty("timePasswordChanged", now);
michael@0 1155 }
michael@0 1156 propBag.setProperty("timeLastUsed", now);
michael@0 1157 propBag.setProperty("timesUsedIncrement", 1);
michael@0 1158 this._pwmgr.modifyLogin(login, propBag);
michael@0 1159 },
michael@0 1160
michael@0 1161
michael@0 1162 /*
michael@0 1163 * _getChromeWindow
michael@0 1164 *
michael@0 1165 * Given a content DOM window, returns the chrome window it's in.
michael@0 1166 */
michael@0 1167 _getChromeWindow: function (aWindow) {
michael@0 1168 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1169 .getInterface(Ci.nsIWebNavigation)
michael@0 1170 .QueryInterface(Ci.nsIDocShell)
michael@0 1171 .chromeEventHandler.ownerDocument.defaultView;
michael@0 1172 return chromeWin;
michael@0 1173 },
michael@0 1174
michael@0 1175
michael@0 1176 /*
michael@0 1177 * _getNotifyWindow
michael@0 1178 */
michael@0 1179 _getNotifyWindow: function () {
michael@0 1180
michael@0 1181 try {
michael@0 1182 // Get topmost window, in case we're in a frame.
michael@0 1183 var notifyWin = this._window.top;
michael@0 1184
michael@0 1185 // Some sites pop up a temporary login window, when disappears
michael@0 1186 // upon submission of credentials. We want to put the notification
michael@0 1187 // bar in the opener window if this seems to be happening.
michael@0 1188 if (notifyWin.opener) {
michael@0 1189 var chromeDoc = this._getChromeWindow(notifyWin).
michael@0 1190 document.documentElement;
michael@0 1191 var webnav = notifyWin.
michael@0 1192 QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 1193 getInterface(Ci.nsIWebNavigation);
michael@0 1194
michael@0 1195 // Check to see if the current window was opened with chrome
michael@0 1196 // disabled, and if so use the opener window. But if the window
michael@0 1197 // has been used to visit other pages (ie, has a history),
michael@0 1198 // assume it'll stick around and *don't* use the opener.
michael@0 1199 if (chromeDoc.getAttribute("chromehidden") &&
michael@0 1200 webnav.sessionHistory.count == 1) {
michael@0 1201 this.log("Using opener window for notification bar.");
michael@0 1202 notifyWin = notifyWin.opener;
michael@0 1203 }
michael@0 1204 }
michael@0 1205
michael@0 1206 return notifyWin;
michael@0 1207
michael@0 1208 } catch (e) {
michael@0 1209 // If any errors happen, just assume no notification box.
michael@0 1210 this.log("Unable to get notify window");
michael@0 1211 return null;
michael@0 1212 }
michael@0 1213 },
michael@0 1214
michael@0 1215
michael@0 1216 /*
michael@0 1217 * _getPopupNote
michael@0 1218 *
michael@0 1219 * Returns the popup notification to this prompter,
michael@0 1220 * or null if there isn't one available.
michael@0 1221 */
michael@0 1222 _getPopupNote : function () {
michael@0 1223 let popupNote = null;
michael@0 1224
michael@0 1225 try {
michael@0 1226 let notifyWin = this._getNotifyWindow();
michael@0 1227
michael@0 1228 // Get the chrome window for the content window we're using.
michael@0 1229 // .wrappedJSObject needed here -- see bug 422974 comment 5.
michael@0 1230 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
michael@0 1231
michael@0 1232 popupNote = chromeWin.PopupNotifications;
michael@0 1233 } catch (e) {
michael@0 1234 this.log("Popup notifications not available on window");
michael@0 1235 }
michael@0 1236
michael@0 1237 return popupNote;
michael@0 1238 },
michael@0 1239
michael@0 1240
michael@0 1241 /*
michael@0 1242 * _getNotifyBox
michael@0 1243 *
michael@0 1244 * Returns the notification box to this prompter, or null if there isn't
michael@0 1245 * a notification box available.
michael@0 1246 */
michael@0 1247 _getNotifyBox : function () {
michael@0 1248 let notifyBox = null;
michael@0 1249
michael@0 1250 try {
michael@0 1251 let notifyWin = this._getNotifyWindow();
michael@0 1252
michael@0 1253 // Get the chrome window for the content window we're using.
michael@0 1254 // .wrappedJSObject needed here -- see bug 422974 comment 5.
michael@0 1255 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
michael@0 1256
michael@0 1257 notifyBox = chromeWin.getNotificationBox(notifyWin);
michael@0 1258 } catch (e) {
michael@0 1259 this.log("Notification bars not available on window");
michael@0 1260 }
michael@0 1261
michael@0 1262 return notifyBox;
michael@0 1263 },
michael@0 1264
michael@0 1265
michael@0 1266 /*
michael@0 1267 * _repickSelectedLogin
michael@0 1268 *
michael@0 1269 * The user might enter a login that isn't the one we prefilled, but
michael@0 1270 * is the same as some other existing login. So, pick a login with a
michael@0 1271 * matching username, or return null.
michael@0 1272 */
michael@0 1273 _repickSelectedLogin : function (foundLogins, username) {
michael@0 1274 for (var i = 0; i < foundLogins.length; i++)
michael@0 1275 if (foundLogins[i].username == username)
michael@0 1276 return foundLogins[i];
michael@0 1277 return null;
michael@0 1278 },
michael@0 1279
michael@0 1280
michael@0 1281 /*
michael@0 1282 * _getLocalizedString
michael@0 1283 *
michael@0 1284 * Can be called as:
michael@0 1285 * _getLocalizedString("key1");
michael@0 1286 * _getLocalizedString("key2", ["arg1"]);
michael@0 1287 * _getLocalizedString("key3", ["arg1", "arg2"]);
michael@0 1288 * (etc)
michael@0 1289 *
michael@0 1290 * Returns the localized string for the specified key,
michael@0 1291 * formatted if required.
michael@0 1292 *
michael@0 1293 */
michael@0 1294 _getLocalizedString : function (key, formatArgs) {
michael@0 1295 if (formatArgs)
michael@0 1296 return this._strBundle.formatStringFromName(
michael@0 1297 key, formatArgs, formatArgs.length);
michael@0 1298 else
michael@0 1299 return this._strBundle.GetStringFromName(key);
michael@0 1300 },
michael@0 1301
michael@0 1302
michael@0 1303 /*
michael@0 1304 * _sanitizeUsername
michael@0 1305 *
michael@0 1306 * Sanitizes the specified username, by stripping quotes and truncating if
michael@0 1307 * it's too long. This helps prevent an evil site from messing with the
michael@0 1308 * "save password?" prompt too much.
michael@0 1309 */
michael@0 1310 _sanitizeUsername : function (username) {
michael@0 1311 if (username.length > 30) {
michael@0 1312 username = username.substring(0, 30);
michael@0 1313 username += this._ellipsis;
michael@0 1314 }
michael@0 1315 return username.replace(/['"]/g, "");
michael@0 1316 },
michael@0 1317
michael@0 1318
michael@0 1319 /*
michael@0 1320 * _getFormattedHostname
michael@0 1321 *
michael@0 1322 * The aURI parameter may either be a string uri, or an nsIURI instance.
michael@0 1323 *
michael@0 1324 * Returns the hostname to use in a nsILoginInfo object (for example,
michael@0 1325 * "http://example.com").
michael@0 1326 */
michael@0 1327 _getFormattedHostname : function (aURI) {
michael@0 1328 var uri;
michael@0 1329 if (aURI instanceof Ci.nsIURI) {
michael@0 1330 uri = aURI;
michael@0 1331 } else {
michael@0 1332 uri = Services.io.newURI(aURI, null, null);
michael@0 1333 }
michael@0 1334 var scheme = uri.scheme;
michael@0 1335
michael@0 1336 var hostname = scheme + "://" + uri.host;
michael@0 1337
michael@0 1338 // If the URI explicitly specified a port, only include it when
michael@0 1339 // it's not the default. (We never want "http://foo.com:80")
michael@0 1340 port = uri.port;
michael@0 1341 if (port != -1) {
michael@0 1342 var handler = Services.io.getProtocolHandler(scheme);
michael@0 1343 if (port != handler.defaultPort)
michael@0 1344 hostname += ":" + port;
michael@0 1345 }
michael@0 1346
michael@0 1347 return hostname;
michael@0 1348 },
michael@0 1349
michael@0 1350
michael@0 1351 /*
michael@0 1352 * _getShortDisplayHost
michael@0 1353 *
michael@0 1354 * Converts a login's hostname field (a URL) to a short string for
michael@0 1355 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
michael@0 1356 * "ftp://www.site.co.uk" --> "site.co.uk".
michael@0 1357 */
michael@0 1358 _getShortDisplayHost: function (aURIString) {
michael@0 1359 var displayHost;
michael@0 1360
michael@0 1361 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
michael@0 1362 getService(Ci.nsIEffectiveTLDService);
michael@0 1363 var idnService = Cc["@mozilla.org/network/idn-service;1"].
michael@0 1364 getService(Ci.nsIIDNService);
michael@0 1365 try {
michael@0 1366 var uri = Services.io.newURI(aURIString, null, null);
michael@0 1367 var baseDomain = eTLDService.getBaseDomain(uri);
michael@0 1368 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
michael@0 1369 } catch (e) {
michael@0 1370 this.log("_getShortDisplayHost couldn't process " + aURIString);
michael@0 1371 }
michael@0 1372
michael@0 1373 if (!displayHost)
michael@0 1374 displayHost = aURIString;
michael@0 1375
michael@0 1376 return displayHost;
michael@0 1377 },
michael@0 1378
michael@0 1379
michael@0 1380 /*
michael@0 1381 * _getAuthTarget
michael@0 1382 *
michael@0 1383 * Returns the hostname and realm for which authentication is being
michael@0 1384 * requested, in the format expected to be used with nsILoginInfo.
michael@0 1385 */
michael@0 1386 _getAuthTarget : function (aChannel, aAuthInfo) {
michael@0 1387 var hostname, realm;
michael@0 1388
michael@0 1389 // If our proxy is demanding authentication, don't use the
michael@0 1390 // channel's actual destination.
michael@0 1391 if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
michael@0 1392 this.log("getAuthTarget is for proxy auth");
michael@0 1393 if (!(aChannel instanceof Ci.nsIProxiedChannel))
michael@0 1394 throw "proxy auth needs nsIProxiedChannel";
michael@0 1395
michael@0 1396 var info = aChannel.proxyInfo;
michael@0 1397 if (!info)
michael@0 1398 throw "proxy auth needs nsIProxyInfo";
michael@0 1399
michael@0 1400 // Proxies don't have a scheme, but we'll use "moz-proxy://"
michael@0 1401 // so that it's more obvious what the login is for.
michael@0 1402 var idnService = Cc["@mozilla.org/network/idn-service;1"].
michael@0 1403 getService(Ci.nsIIDNService);
michael@0 1404 hostname = "moz-proxy://" +
michael@0 1405 idnService.convertUTF8toACE(info.host) +
michael@0 1406 ":" + info.port;
michael@0 1407 realm = aAuthInfo.realm;
michael@0 1408 if (!realm)
michael@0 1409 realm = hostname;
michael@0 1410
michael@0 1411 return [hostname, realm];
michael@0 1412 }
michael@0 1413
michael@0 1414 hostname = this._getFormattedHostname(aChannel.URI);
michael@0 1415
michael@0 1416 // If a HTTP WWW-Authenticate header specified a realm, that value
michael@0 1417 // will be available here. If it wasn't set or wasn't HTTP, we'll use
michael@0 1418 // the formatted hostname instead.
michael@0 1419 realm = aAuthInfo.realm;
michael@0 1420 if (!realm)
michael@0 1421 realm = hostname;
michael@0 1422
michael@0 1423 return [hostname, realm];
michael@0 1424 },
michael@0 1425
michael@0 1426
michael@0 1427 /**
michael@0 1428 * Returns [username, password] as extracted from aAuthInfo (which
michael@0 1429 * holds this info after having prompted the user).
michael@0 1430 *
michael@0 1431 * If the authentication was for a Windows domain, we'll prepend the
michael@0 1432 * return username with the domain. (eg, "domain\user")
michael@0 1433 */
michael@0 1434 _GetAuthInfo : function (aAuthInfo) {
michael@0 1435 var username, password;
michael@0 1436
michael@0 1437 var flags = aAuthInfo.flags;
michael@0 1438 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
michael@0 1439 username = aAuthInfo.domain + "\\" + aAuthInfo.username;
michael@0 1440 else
michael@0 1441 username = aAuthInfo.username;
michael@0 1442
michael@0 1443 password = aAuthInfo.password;
michael@0 1444
michael@0 1445 return [username, password];
michael@0 1446 },
michael@0 1447
michael@0 1448
michael@0 1449 /**
michael@0 1450 * Given a username (possibly in DOMAIN\user form) and password, parses the
michael@0 1451 * domain out of the username if necessary and sets domain, username and
michael@0 1452 * password on the auth information object.
michael@0 1453 */
michael@0 1454 _SetAuthInfo : function (aAuthInfo, username, password) {
michael@0 1455 var flags = aAuthInfo.flags;
michael@0 1456 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
michael@0 1457 // Domain is separated from username by a backslash
michael@0 1458 var idx = username.indexOf("\\");
michael@0 1459 if (idx == -1) {
michael@0 1460 aAuthInfo.username = username;
michael@0 1461 } else {
michael@0 1462 aAuthInfo.domain = username.substring(0, idx);
michael@0 1463 aAuthInfo.username = username.substring(idx+1);
michael@0 1464 }
michael@0 1465 } else {
michael@0 1466 aAuthInfo.username = username;
michael@0 1467 }
michael@0 1468 aAuthInfo.password = password;
michael@0 1469 },
michael@0 1470
michael@0 1471 _newAsyncPromptConsumer : function(aCallback, aContext) {
michael@0 1472 return {
michael@0 1473 QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
michael@0 1474 callback: aCallback,
michael@0 1475 context: aContext,
michael@0 1476 cancel: function() {
michael@0 1477 this.callback.onAuthCancelled(this.context, false);
michael@0 1478 this.callback = null;
michael@0 1479 this.context = null;
michael@0 1480 }
michael@0 1481 }
michael@0 1482 }
michael@0 1483
michael@0 1484 }; // end of LoginManagerPrompter implementation
michael@0 1485
michael@0 1486
michael@0 1487 var component = [LoginManagerPromptFactory, LoginManagerPrompter];
michael@0 1488 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial