Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
const { XPCOMUtils } = ChromeUtils.import(
7
);
8
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9
const { PrivateBrowsingUtils } = ChromeUtils.import(
11
);
12
const { PromptUtils } = ChromeUtils.import(
14
);
15
16
/* eslint-disable block-scoped-var, no-var */
17
18
ChromeUtils.defineModuleGetter(
19
this,
20
"LoginHelper",
22
);
23
24
const LoginInfo = Components.Constructor(
25
"@mozilla.org/login-manager/loginInfo;1",
26
"nsILoginInfo",
27
"init"
28
);
29
30
const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
31
32
/**
33
* The maximum age of the password in ms (using `timePasswordChanged`) whereby
34
* a user can toggle the password visibility in a doorhanger to add a username to
35
* a saved login.
36
*/
37
const VISIBILITY_TOGGLE_MAX_PW_AGE_MS = 2 * 60 * 1000; // 2 minutes
38
39
/**
40
* Constants for password prompt telemetry.
41
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
42
const PROMPT_DISPLAYED = 0;
43
44
const PROMPT_ADD_OR_UPDATE = 1;
45
const PROMPT_NOTNOW = 2;
46
const PROMPT_NEVER = 3;
47
48
/**
49
* The minimum age of a doorhanger in ms before it will get removed after a locationchange
50
*/
51
const NOTIFICATION_TIMEOUT_MS = 10 * 1000; // 10 seconds
52
53
/**
54
* The minimum age of an attention-requiring dismissed doorhanger in ms
55
* before it will get removed after a locationchange
56
*/
57
const ATTENTION_NOTIFICATION_TIMEOUT_MS = 60 * 1000; // 1 minute
58
59
/**
60
* A helper module to prevent modal auth prompt abuse.
61
*/
62
const PromptAbuseHelper = {
63
getBaseDomainOrFallback(hostname) {
64
try {
65
return Services.eTLD.getBaseDomainFromHost(hostname);
66
} catch (e) {
67
return hostname;
68
}
69
},
70
71
incrementPromptAbuseCounter(baseDomain, browser) {
72
if (!browser) {
73
return;
74
}
75
76
if (!browser.authPromptAbuseCounter) {
77
browser.authPromptAbuseCounter = {};
78
}
79
80
if (!browser.authPromptAbuseCounter[baseDomain]) {
81
browser.authPromptAbuseCounter[baseDomain] = 0;
82
}
83
84
browser.authPromptAbuseCounter[baseDomain] += 1;
85
},
86
87
resetPromptAbuseCounter(baseDomain, browser) {
88
if (!browser || !browser.authPromptAbuseCounter) {
89
return;
90
}
91
92
browser.authPromptAbuseCounter[baseDomain] = 0;
93
},
94
95
hasReachedAbuseLimit(baseDomain, browser) {
96
if (!browser || !browser.authPromptAbuseCounter) {
97
return false;
98
}
99
100
let abuseCounter = browser.authPromptAbuseCounter[baseDomain];
101
// Allow for setting -1 to turn the feature off.
102
if (this.abuseLimit < 0) {
103
return false;
104
}
105
return !!abuseCounter && abuseCounter >= this.abuseLimit;
106
},
107
};
108
109
XPCOMUtils.defineLazyPreferenceGetter(
110
PromptAbuseHelper,
111
"abuseLimit",
112
"prompts.authentication_dialog_abuse_limit"
113
);
114
115
/**
116
* Implements nsIPromptFactory
117
*
118
* Invoked by [toolkit/components/prompts/src/nsPrompter.js]
119
*/
120
function LoginManagerPromptFactory() {
121
Services.obs.addObserver(this, "quit-application-granted", true);
122
Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
123
Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
124
}
125
126
LoginManagerPromptFactory.prototype = {
127
classID: Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
128
QueryInterface: ChromeUtils.generateQI([
129
Ci.nsIPromptFactory,
130
Ci.nsIObserver,
131
Ci.nsISupportsWeakReference,
132
]),
133
134
_asyncPrompts: {},
135
_asyncPromptInProgress: false,
136
137
observe(subject, topic, data) {
138
this.log("Observed: " + topic);
139
if (topic == "quit-application-granted") {
140
this._cancelPendingPrompts();
141
} else if (topic == "passwordmgr-crypto-login") {
142
// Start processing the deferred prompters.
143
this._doAsyncPrompt();
144
} else if (topic == "passwordmgr-crypto-loginCanceled") {
145
// User canceled a Master Password prompt, so go ahead and cancel
146
// all pending auth prompts to avoid nagging over and over.
147
this._cancelPendingPrompts();
148
}
149
},
150
151
getPrompt(aWindow, aIID) {
152
var prompt = new LoginManagerPrompter().QueryInterface(aIID);
153
prompt.init(aWindow, this);
154
return prompt;
155
},
156
157
_doAsyncPrompt() {
158
if (this._asyncPromptInProgress) {
159
this.log("_doAsyncPrompt bypassed, already in progress");
160
return;
161
}
162
163
// Find the first prompt key we have in the queue
164
var hashKey = null;
165
for (hashKey in this._asyncPrompts) {
166
break;
167
}
168
169
if (!hashKey) {
170
this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
171
return;
172
}
173
174
// If login manger has logins for this host, defer prompting if we're
175
// already waiting on a master password entry.
176
var prompt = this._asyncPrompts[hashKey];
177
var prompter = prompt.prompter;
178
var [origin, httpRealm] = prompter._getAuthTarget(
179
prompt.channel,
180
prompt.authInfo
181
);
182
var hasLogins = Services.logins.countLogins(origin, null, httpRealm) > 0;
183
if (
184
!hasLogins &&
185
LoginHelper.schemeUpgrades &&
186
origin.startsWith("https://")
187
) {
188
let httpOrigin = origin.replace(/^https:\/\//, "http://");
189
hasLogins = Services.logins.countLogins(httpOrigin, null, httpRealm) > 0;
190
}
191
if (hasLogins && Services.logins.uiBusy) {
192
this.log("_doAsyncPrompt:run bypassed, master password UI busy");
193
return;
194
}
195
196
var self = this;
197
198
var runnable = {
199
cancel: false,
200
run() {
201
var ok = false;
202
if (!this.cancel) {
203
try {
204
self.log(
205
"_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"
206
);
207
ok = prompter.promptAuth(
208
prompt.channel,
209
prompt.level,
210
prompt.authInfo
211
);
212
} catch (e) {
213
if (
214
e instanceof Components.Exception &&
215
e.result == Cr.NS_ERROR_NOT_AVAILABLE
216
) {
217
self.log(
218
"_doAsyncPrompt:run bypassed, UI is not available in this context"
219
);
220
} else {
221
Cu.reportError(
222
"LoginManagerPrompter: _doAsyncPrompt:run: " + e + "\n"
223
);
224
}
225
}
226
227
delete self._asyncPrompts[hashKey];
228
prompt.inProgress = false;
229
self._asyncPromptInProgress = false;
230
}
231
232
for (var consumer of prompt.consumers) {
233
if (!consumer.callback) {
234
// Not having a callback means that consumer didn't provide it
235
// or canceled the notification
236
continue;
237
}
238
239
self.log("Calling back to " + consumer.callback + " ok=" + ok);
240
try {
241
if (ok) {
242
consumer.callback.onAuthAvailable(
243
consumer.context,
244
prompt.authInfo
245
);
246
} else {
247
consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
248
}
249
} catch (e) {
250
/* Throw away exceptions caused by callback */
251
}
252
}
253
self._doAsyncPrompt();
254
},
255
};
256
257
this._asyncPromptInProgress = true;
258
prompt.inProgress = true;
259
260
Services.tm.dispatchToMainThread(runnable);
261
this.log("_doAsyncPrompt:run dispatched");
262
},
263
264
_cancelPendingPrompts() {
265
this.log("Canceling all pending prompts...");
266
var asyncPrompts = this._asyncPrompts;
267
this.__proto__._asyncPrompts = {};
268
269
for (var hashKey in asyncPrompts) {
270
let prompt = asyncPrompts[hashKey];
271
// Watch out! If this prompt is currently prompting, let it handle
272
// notifying the callbacks of success/failure, since it's already
273
// asking the user for input. Reusing a callback can be crashy.
274
if (prompt.inProgress) {
275
this.log("skipping a prompt in progress");
276
continue;
277
}
278
279
for (var consumer of prompt.consumers) {
280
if (!consumer.callback) {
281
continue;
282
}
283
284
this.log("Canceling async auth prompt callback " + consumer.callback);
285
try {
286
consumer.callback.onAuthCancelled(consumer.context, true);
287
} catch (e) {
288
/* Just ignore exceptions from the callback */
289
}
290
}
291
}
292
},
293
}; // end of LoginManagerPromptFactory implementation
294
295
XPCOMUtils.defineLazyGetter(
296
this.LoginManagerPromptFactory.prototype,
297
"log",
298
() => {
299
let logger = LoginHelper.createLogger("Login PromptFactory");
300
return logger.log.bind(logger);
301
}
302
);
303
304
/* ==================== LoginManagerPrompter ==================== */
305
306
/**
307
* Implements interfaces for prompting the user to enter/save/change auth info.
308
*
309
* nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
310
*
311
* nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
312
* (eg HTTP Authenticate, FTP login).
313
*
314
* nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
315
* found in HTML forms.
316
*/
317
function LoginManagerPrompter() {}
318
319
LoginManagerPrompter.prototype = {
320
classID: Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
321
QueryInterface: ChromeUtils.generateQI([
322
Ci.nsIAuthPrompt,
323
Ci.nsIAuthPrompt2,
324
Ci.nsILoginManagerPrompter,
325
]),
326
327
_factory: null,
328
_chromeWindow: null,
329
_browser: null,
330
_openerBrowser: null,
331
332
__strBundle: null, // String bundle for L10N
333
get _strBundle() {
334
if (!this.__strBundle) {
335
this.__strBundle = Services.strings.createBundle(
337
);
338
if (!this.__strBundle) {
339
throw new Error("String bundle for Login Manager not present!");
340
}
341
}
342
343
return this.__strBundle;
344
},
345
346
__ellipsis: null,
347
get _ellipsis() {
348
if (!this.__ellipsis) {
349
this.__ellipsis = "\u2026";
350
try {
351
this.__ellipsis = Services.prefs.getComplexValue(
352
"intl.ellipsis",
353
Ci.nsIPrefLocalizedString
354
).data;
355
} catch (e) {}
356
}
357
return this.__ellipsis;
358
},
359
360
// Whether we are in private browsing mode
361
get _inPrivateBrowsing() {
362
if (this._chromeWindow) {
363
return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
364
}
365
// If we don't that we're in private browsing mode if the caller did
366
// not provide a window. The callers which really care about this
367
// will indeed pass down a window to us, and for those who don't,
368
// we can just assume that we don't want to save the entered login
369
// information.
370
this.log("We have no chromeWindow so assume we're in a private context");
371
return true;
372
},
373
374
get _allowRememberLogin() {
375
if (!this._inPrivateBrowsing) {
376
return true;
377
}
378
return LoginHelper.privateBrowsingCaptureEnabled;
379
},
380
381
/* ---------- nsIAuthPrompt prompts ---------- */
382
383
/**
384
* Wrapper around the prompt service prompt. Saving random fields here
385
* doesn't really make sense and therefore isn't implemented.
386
*/
387
prompt(
388
aDialogTitle,
389
aText,
390
aPasswordRealm,
391
aSavePassword,
392
aDefaultText,
393
aResult
394
) {
395
if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
396
throw new Components.Exception(
397
"prompt only supports SAVE_PASSWORD_NEVER",
398
Cr.NS_ERROR_NOT_IMPLEMENTED
399
);
400
}
401
402
this.log("===== prompt() called =====");
403
404
if (aDefaultText) {
405
aResult.value = aDefaultText;
406
}
407
408
return Services.prompt.prompt(
409
this._chromeWindow,
410
aDialogTitle,
411
aText,
412
aResult,
413
null,
414
{}
415
);
416
},
417
418
/**
419
* Looks up a username and password in the database. Will prompt the user
420
* with a dialog, even if a username and password are found.
421
*/
422
promptUsernameAndPassword(
423
aDialogTitle,
424
aText,
425
aPasswordRealm,
426
aSavePassword,
427
aUsername,
428
aPassword
429
) {
430
this.log("===== promptUsernameAndPassword() called =====");
431
432
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
433
throw new Components.Exception(
434
"promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
435
Cr.NS_ERROR_NOT_IMPLEMENTED
436
);
437
}
438
439
let foundLogins = null;
440
var selectedLogin = null;
441
var checkBox = { value: false };
442
var checkBoxLabel = null;
443
var [origin, realm, unused] = this._getRealmInfo(aPasswordRealm);
444
445
// If origin is null, we can't save this login.
446
if (origin) {
447
var canRememberLogin = false;
448
if (this._allowRememberLogin) {
449
canRememberLogin =
450
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
451
Services.logins.getLoginSavingEnabled(origin);
452
}
453
454
// if checkBoxLabel is null, the checkbox won't be shown at all.
455
if (canRememberLogin) {
456
checkBoxLabel = this._getLocalizedString("rememberPassword");
457
}
458
459
// Look for existing logins.
460
foundLogins = Services.logins.findLogins(origin, null, realm);
461
462
// XXX Like the original code, we can't deal with multiple
463
// account selection. (bug 227632)
464
if (foundLogins.length) {
465
selectedLogin = foundLogins[0];
466
467
// If the caller provided a username, try to use it. If they
468
// provided only a password, this will try to find a password-only
469
// login (or return null if none exists).
470
if (aUsername.value) {
471
selectedLogin = this._repickSelectedLogin(
472
foundLogins,
473
aUsername.value
474
);
475
}
476
477
if (selectedLogin) {
478
checkBox.value = true;
479
aUsername.value = selectedLogin.username;
480
// If the caller provided a password, prefer it.
481
if (!aPassword.value) {
482
aPassword.value = selectedLogin.password;
483
}
484
}
485
}
486
}
487
488
var ok = Services.prompt.promptUsernameAndPassword(
489
this._chromeWindow,
490
aDialogTitle,
491
aText,
492
aUsername,
493
aPassword,
494
checkBoxLabel,
495
checkBox
496
);
497
498
if (!ok || !checkBox.value || !origin) {
499
return ok;
500
}
501
502
if (!aPassword.value) {
503
this.log("No password entered, so won't offer to save.");
504
return ok;
505
}
506
507
// XXX We can't prompt with multiple logins yet (bug 227632), so
508
// the entered login might correspond to an existing login
509
// other than the one we originally selected.
510
selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
511
512
// If we didn't find an existing login, or if the username
513
// changed, save as a new login.
514
let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
515
Ci.nsILoginInfo
516
);
517
newLogin.init(
518
origin,
519
null,
520
realm,
521
aUsername.value,
522
aPassword.value,
523
"",
524
""
525
);
526
if (!selectedLogin) {
527
// add as new
528
this.log("New login seen for " + realm);
529
Services.logins.addLogin(newLogin);
530
} else if (aPassword.value != selectedLogin.password) {
531
// update password
532
this.log("Updating password for " + realm);
533
this._updateLogin(selectedLogin, newLogin);
534
} else {
535
this.log("Login unchanged, no further action needed.");
536
this._updateLogin(selectedLogin);
537
}
538
539
return ok;
540
},
541
542
/**
543
* If a password is found in the database for the password realm, it is
544
* returned straight away without displaying a dialog.
545
*
546
* If a password is not found in the database, the user will be prompted
547
* with a dialog with a text field and ok/cancel buttons. If the user
548
* allows it, then the password will be saved in the database.
549
*/
550
promptPassword(
551
aDialogTitle,
552
aText,
553
aPasswordRealm,
554
aSavePassword,
555
aPassword
556
) {
557
this.log("===== promptPassword called() =====");
558
559
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
560
throw new Components.Exception(
561
"promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
562
Cr.NS_ERROR_NOT_IMPLEMENTED
563
);
564
}
565
566
var checkBox = { value: false };
567
var checkBoxLabel = null;
568
var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
569
570
username = decodeURIComponent(username);
571
572
// If origin is null, we can't save this login.
573
if (origin && !this._inPrivateBrowsing) {
574
var canRememberLogin =
575
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
576
Services.logins.getLoginSavingEnabled(origin);
577
578
// if checkBoxLabel is null, the checkbox won't be shown at all.
579
if (canRememberLogin) {
580
checkBoxLabel = this._getLocalizedString("rememberPassword");
581
}
582
583
if (!aPassword.value) {
584
// Look for existing logins.
585
var foundLogins = Services.logins.findLogins(origin, null, realm);
586
587
// XXX Like the original code, we can't deal with multiple
588
// account selection (bug 227632). We can deal with finding the
589
// account based on the supplied username - but in this case we'll
590
// just return the first match.
591
for (var i = 0; i < foundLogins.length; ++i) {
592
if (foundLogins[i].username == username) {
593
aPassword.value = foundLogins[i].password;
594
// wallet returned straight away, so this mimics that code
595
return true;
596
}
597
}
598
}
599
}
600
601
var ok = Services.prompt.promptPassword(
602
this._chromeWindow,
603
aDialogTitle,
604
aText,
605
aPassword,
606
checkBoxLabel,
607
checkBox
608
);
609
610
if (ok && checkBox.value && origin && aPassword.value) {
611
var newLogin = Cc[
612
"@mozilla.org/login-manager/loginInfo;1"
613
].createInstance(Ci.nsILoginInfo);
614
newLogin.init(origin, null, realm, username, aPassword.value, "", "");
615
616
this.log("New login seen for " + realm);
617
618
Services.logins.addLogin(newLogin);
619
}
620
621
return ok;
622
},
623
624
/* ---------- nsIAuthPrompt helpers ---------- */
625
626
/**
627
* Given aRealmString, such as "http://user@example.com/foo", returns an
628
* array of:
629
* - the formatted origin
630
* - the realm (origin + path)
631
* - the username, if present
632
*
633
* If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
634
* channels, e.g. "example.com:80 (httprealm)", null is returned for all
635
* arguments to let callers know the login can't be saved because we don't
636
* know whether it's http or https.
637
*/
638
_getRealmInfo(aRealmString) {
639
var httpRealm = /^.+ \(.+\)$/;
640
if (httpRealm.test(aRealmString)) {
641
return [null, null, null];
642
}
643
644
var uri = Services.io.newURI(aRealmString);
645
var pathname = "";
646
647
if (uri.pathQueryRef != "/") {
648
pathname = uri.pathQueryRef;
649
}
650
651
var formattedOrigin = this._getFormattedOrigin(uri);
652
653
return [formattedOrigin, formattedOrigin + pathname, uri.username];
654
},
655
656
/* ---------- nsIAuthPrompt2 prompts ---------- */
657
658
/**
659
* Implementation of nsIAuthPrompt2.
660
*
661
* @param {nsIChannel} aChannel
662
* @param {int} aLevel
663
* @param {nsIAuthInformation} aAuthInfo
664
*/
665
promptAuth(aChannel, aLevel, aAuthInfo) {
666
var selectedLogin = null;
667
var checkbox = { value: false };
668
var checkboxLabel = null;
669
var epicfail = false;
670
var canAutologin = false;
671
var notifyObj;
672
var foundLogins;
673
674
try {
675
this.log("===== promptAuth called =====");
676
677
// If the user submits a login but it fails, we need to remove the
678
// notification prompt that was displayed. Conveniently, the user will
679
// be prompted for authentication again, which brings us here.
680
this._removeLoginNotifications();
681
682
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
683
684
// Looks for existing logins to prefill the prompt with.
685
foundLogins = LoginHelper.searchLoginsWithObject({
686
origin,
687
httpRealm,
688
schemeUpgrades: LoginHelper.schemeUpgrades,
689
});
690
this.log("found", foundLogins.length, "matching logins.");
691
let resolveBy = ["scheme", "timePasswordChanged"];
692
foundLogins = LoginHelper.dedupeLogins(
693
foundLogins,
694
["username"],
695
resolveBy,
696
origin
697
);
698
this.log(foundLogins.length, "matching logins remain after deduping");
699
700
// XXX Can't select from multiple accounts yet. (bug 227632)
701
if (foundLogins.length) {
702
selectedLogin = foundLogins[0];
703
this._SetAuthInfo(
704
aAuthInfo,
705
selectedLogin.username,
706
selectedLogin.password
707
);
708
709
// Allow automatic proxy login
710
if (
711
aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
712
!(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
713
Services.prefs.getBoolPref("signon.autologin.proxy") &&
714
!this._inPrivateBrowsing
715
) {
716
this.log("Autologin enabled, skipping auth prompt.");
717
canAutologin = true;
718
}
719
720
checkbox.value = true;
721
}
722
723
var canRememberLogin = Services.logins.getLoginSavingEnabled(origin);
724
if (!this._allowRememberLogin) {
725
canRememberLogin = false;
726
}
727
728
// if checkboxLabel is null, the checkbox won't be shown at all.
729
notifyObj = this._getPopupNote();
730
if (canRememberLogin && !notifyObj) {
731
checkboxLabel = this._getLocalizedString("rememberPassword");
732
}
733
} catch (e) {
734
// Ignore any errors and display the prompt anyway.
735
epicfail = true;
736
Cu.reportError(
737
"LoginManagerPrompter: Epic fail in promptAuth: " + e + "\n"
738
);
739
}
740
741
var ok = canAutologin;
742
let browser = this._browser;
743
let baseDomain;
744
745
// We might not have a browser or browser.currentURI.host could fail
746
// (e.g. on about:blank). Fall back to the subresource hostname in that case.
747
try {
748
let topLevelHost = browser.currentURI.host;
749
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(topLevelHost);
750
} catch (e) {
751
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(origin);
752
}
753
754
if (!ok) {
755
if (PromptAbuseHelper.hasReachedAbuseLimit(baseDomain, browser)) {
756
this.log("Blocking auth dialog, due to exceeding dialog bloat limit");
757
return false;
758
}
759
760
// Set up a counter for ensuring that the basic auth prompt can not
761
// be abused for DOS-style attacks. With this counter, each eTLD+1
762
// per browser will get a limited number of times a user can
763
// cancel the prompt until we stop showing it.
764
PromptAbuseHelper.incrementPromptAbuseCounter(baseDomain, browser);
765
766
if (this._chromeWindow) {
767
PromptUtils.fireDialogEvent(
768
this._chromeWindow,
769
"DOMWillOpenModalDialog",
770
this._browser
771
);
772
}
773
ok = Services.prompt.promptAuth(
774
this._chromeWindow,
775
aChannel,
776
aLevel,
777
aAuthInfo,
778
checkboxLabel,
779
checkbox
780
);
781
}
782
783
let [username, password] = this._GetAuthInfo(aAuthInfo);
784
785
// Reset the counter state if the user replied to a prompt and actually
786
// tried to login (vs. simply clicking any button to get out).
787
if (ok && (username || password)) {
788
PromptAbuseHelper.resetPromptAbuseCounter(baseDomain, browser);
789
}
790
791
// If there's a notification prompt, use it to allow the user to
792
// determine if the login should be saved. If there isn't a
793
// notification prompt, only save the login if the user set the
794
// checkbox to do so.
795
var rememberLogin = notifyObj ? canRememberLogin : checkbox.value;
796
if (!ok || !rememberLogin || epicfail) {
797
return ok;
798
}
799
800
try {
801
if (!password) {
802
this.log("No password entered, so won't offer to save.");
803
return ok;
804
}
805
806
// XXX We can't prompt with multiple logins yet (bug 227632), so
807
// the entered login might correspond to an existing login
808
// other than the one we originally selected.
809
selectedLogin = this._repickSelectedLogin(foundLogins, username);
810
811
// If we didn't find an existing login, or if the username
812
// changed, save as a new login.
813
let newLogin = Cc[
814
"@mozilla.org/login-manager/loginInfo;1"
815
].createInstance(Ci.nsILoginInfo);
816
newLogin.init(origin, null, httpRealm, username, password, "", "");
817
if (!selectedLogin) {
818
this.log(
819
"New login seen for " +
820
username +
821
" @ " +
822
origin +
823
" (" +
824
httpRealm +
825
")"
826
);
827
828
if (notifyObj) {
829
this._showLoginCaptureDoorhanger(newLogin, "password-save", {
830
dismissed: this._inPrivateBrowsing,
831
});
832
Services.obs.notifyObservers(newLogin, "passwordmgr-prompt-save");
833
} else {
834
Services.logins.addLogin(newLogin);
835
}
836
} else if (password != selectedLogin.password) {
837
this.log(
838
"Updating password for " +
839
username +
840
" @ " +
841
origin +
842
" (" +
843
httpRealm +
844
")"
845
);
846
if (notifyObj) {
847
this._showChangeLoginNotification(notifyObj, selectedLogin, newLogin);
848
} else {
849
this._updateLogin(selectedLogin, newLogin);
850
}
851
} else {
852
this.log("Login unchanged, no further action needed.");
853
this._updateLogin(selectedLogin);
854
}
855
} catch (e) {
856
Cu.reportError("LoginManagerPrompter: Fail2 in promptAuth: " + e + "\n");
857
}
858
859
return ok;
860
},
861
862
asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
863
var cancelable = null;
864
865
try {
866
this.log("===== asyncPromptAuth called =====");
867
868
// If the user submits a login but it fails, we need to remove the
869
// notification prompt that was displayed. Conveniently, the user will
870
// be prompted for authentication again, which brings us here.
871
this._removeLoginNotifications();
872
873
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
874
875
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
876
877
var hashKey = aLevel + "|" + origin + "|" + httpRealm;
878
this.log("Async prompt key = " + hashKey);
879
var asyncPrompt = this._factory._asyncPrompts[hashKey];
880
if (asyncPrompt) {
881
this.log(
882
"Prompt bound to an existing one in the queue, callback = " +
883
aCallback
884
);
885
asyncPrompt.consumers.push(cancelable);
886
return cancelable;
887
}
888
889
this.log("Adding new prompt to the queue, callback = " + aCallback);
890
asyncPrompt = {
891
consumers: [cancelable],
892
channel: aChannel,
893
authInfo: aAuthInfo,
894
level: aLevel,
895
inProgress: false,
896
prompter: this,
897
};
898
899
this._factory._asyncPrompts[hashKey] = asyncPrompt;
900
this._factory._doAsyncPrompt();
901
} catch (e) {
902
Cu.reportError(
903
"LoginManagerPrompter: " +
904
"asyncPromptAuth: " +
905
e +
906
"\nFalling back to promptAuth\n"
907
);
908
// Fail the prompt operation to let the consumer fall back
909
// to synchronous promptAuth method
910
throw e;
911
}
912
913
return cancelable;
914
},
915
916
/* ---------- nsILoginManagerPrompter prompts ---------- */
917
918
init(aWindow = null, aFactory = null) {
919
if (!aWindow) {
920
// There may be no applicable window e.g. in a Sandbox or JSM.
921
this._chromeWindow = null;
922
this._browser = null;
923
} else if (aWindow.isChromeWindow) {
924
this._chromeWindow = aWindow;
925
// needs to be set explicitly using setBrowser
926
this._browser = null;
927
} else {
928
let { win, browser } = this._getChromeWindow(aWindow);
929
this._chromeWindow = win;
930
this._browser = browser;
931
}
932
this._openerBrowser = null;
933
this._factory = aFactory || null;
934
935
this.log("===== initialized =====");
936
},
937
938
set browser(aBrowser) {
939
this._browser = aBrowser;
940
},
941
942
set openerBrowser(aOpenerBrowser) {
943
this._openerBrowser = aOpenerBrowser;
944
},
945
946
promptToSavePassword(aLogin, dismissed = false, notifySaved = false) {
947
this.log("promptToSavePassword");
948
var notifyObj = this._getPopupNote();
949
if (notifyObj) {
950
this._showLoginCaptureDoorhanger(
951
aLogin,
952
"password-save",
953
{
954
dismissed: this._inPrivateBrowsing || dismissed,
955
extraAttr: notifySaved ? "attention" : "",
956
},
957
{
958
notifySaved,
959
}
960
);
961
Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
962
} else {
963
this._showSaveLoginDialog(aLogin);
964
}
965
},
966
967
/**
968
* Displays the PopupNotifications.jsm doorhanger for password save or change.
969
*
970
* @param {nsILoginInfo} login
971
* Login to save or change. For changes, this login should contain the
972
* new password and/or username
973
* @param {string} type
974
* This is "password-save" or "password-change" depending on the
975
* original notification type. This is used for telemetry and tests.
976
* @param {object} showOptions
977
* Options to pass along to PopupNotifications.show().
978
* @param {bool} [options.notifySaved = false]
979
* Whether to indicate to the user that the login was already saved.
980
* @param {string} [options.messageStringID = undefined]
981
* An optional string ID to override the default message.
982
* @param {string} [options.autoSavedLoginGuid = ""]
983
* A string guid value for the auto-saved login to be removed if the changes
984
* match it to an different login
985
*/
986
_showLoginCaptureDoorhanger(
987
login,
988
type,
989
showOptions = {},
990
{ notifySaved = false, messageStringID, autoSavedLoginGuid = "" } = {}
991
) {
992
let { browser } = this._getNotifyWindow();
993
if (!browser) {
994
return;
995
}
996
this.log(
997
`_showLoginCaptureDoorhanger, got autoSavedLoginGuid: ${autoSavedLoginGuid}`
998
);
999
1000
let saveMsgNames = {
1001
prompt: login.username === "" ? "saveLoginMsgNoUser" : "saveLoginMsg",
1002
buttonLabel: "saveLoginButtonAllow.label",
1003
buttonAccessKey: "saveLoginButtonAllow.accesskey",
1004
secondaryButtonLabel: "saveLoginButtonDeny.label",
1005
secondaryButtonAccessKey: "saveLoginButtonDeny.accesskey",
1006
};
1007
1008
let changeMsgNames = {
1009
prompt: login.username === "" ? "updateLoginMsgNoUser" : "updateLoginMsg",
1010
buttonLabel: "updateLoginButtonText",
1011
buttonAccessKey: "updateLoginButtonAccessKey",
1012
secondaryButtonLabel: "updateLoginButtonDeny.label",
1013
secondaryButtonAccessKey: "updateLoginButtonDeny.accesskey",
1014
};
1015
1016
let initialMsgNames =
1017
type == "password-save" ? saveMsgNames : changeMsgNames;
1018
1019
if (messageStringID) {
1020
changeMsgNames.prompt = messageStringID;
1021
}
1022
1023
let brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
1024
let brandShortName = brandBundle.GetStringFromName("brandShortName");
1025
let host = this._getShortDisplayHost(login.origin);
1026
let promptMsg =
1027
type == "password-save"
1028
? this._getLocalizedString(saveMsgNames.prompt, [brandShortName, host])
1029
: this._getLocalizedString(changeMsgNames.prompt);
1030
1031
let histogramName =
1032
type == "password-save"
1033
? "PWMGR_PROMPT_REMEMBER_ACTION"
1034
: "PWMGR_PROMPT_UPDATE_ACTION";
1035
let histogram = Services.telemetry.getHistogramById(histogramName);
1036
histogram.add(PROMPT_DISPLAYED);
1037
Services.obs.notifyObservers(
1038
null,
1039
"weave:telemetry:histogram",
1040
histogramName
1041
);
1042
1043
let chromeDoc = browser.ownerDocument;
1044
let currentNotification;
1045
1046
let updateButtonStatus = element => {
1047
let mainActionButton = element.button;
1048
// Disable the main button inside the menu-button if the password field is empty.
1049
if (!login.password.length) {
1050
mainActionButton.setAttribute("disabled", true);
1051
chromeDoc
1052
.getElementById("password-notification-password")
1053
.classList.add("popup-notification-invalid-input");
1054
} else {
1055
mainActionButton.removeAttribute("disabled");
1056
chromeDoc
1057
.getElementById("password-notification-password")
1058
.classList.remove("popup-notification-invalid-input");
1059
}
1060
};
1061
1062
let updateButtonLabel = () => {
1063
if (!currentNotification) {
1064
Cu.reportError("updateButtonLabel, no currentNotification");
1065
}
1066
let foundLogins = LoginHelper.searchLoginsWithObject({
1067
formActionOrigin: login.formActionOrigin,
1068
origin: login.origin,
1069
httpRealm: login.httpRealm,
1070
schemeUpgrades: LoginHelper.schemeUpgrades,
1071
});
1072
1073
let logins = this._filterUpdatableLogins(
1074
login,
1075
foundLogins,
1076
autoSavedLoginGuid
1077
);
1078
let msgNames = !logins.length ? saveMsgNames : changeMsgNames;
1079
1080
// Update the label based on whether this will be a new login or not.
1081
let label = this._getLocalizedString(msgNames.buttonLabel);
1082
let accessKey = this._getLocalizedString(msgNames.buttonAccessKey);
1083
1084
// Update the labels for the next time the panel is opened.
1085
currentNotification.mainAction.label = label;
1086
currentNotification.mainAction.accessKey = accessKey;
1087
1088
// Update the labels in real time if the notification is displayed.
1089
let element = [...currentNotification.owner.panel.childNodes].find(
1090
n => n.notification == currentNotification
1091
);
1092
if (element) {
1093
element.setAttribute("buttonlabel", label);
1094
element.setAttribute("buttonaccesskey", accessKey);
1095
updateButtonStatus(element);
1096
}
1097
};
1098
1099
let writeDataToUI = () => {
1100
let nameField = chromeDoc.getElementById(
1101
"password-notification-username"
1102
);
1103
nameField.placeholder = usernamePlaceholder;
1104
nameField.value = login.username;
1105
1106
let toggleCheckbox = chromeDoc.getElementById(
1107
"password-notification-visibilityToggle"
1108
);
1109
toggleCheckbox.removeAttribute("checked");
1110
let passwordField = chromeDoc.getElementById(
1111
"password-notification-password"
1112
);
1113
// Ensure the type is reset so the field is masked.
1114
passwordField.type = "password";
1115
passwordField.value = login.password;
1116
updateButtonLabel();
1117
};
1118
1119
let readDataFromUI = () => {
1120
login.username = chromeDoc.getElementById(
1121
"password-notification-username"
1122
).value;
1123
login.password = chromeDoc.getElementById(
1124
"password-notification-password"
1125
).value;
1126
};
1127
1128
let onInput = () => {
1129
readDataFromUI();
1130
updateButtonLabel();
1131
};
1132
1133
let onKeyUp = e => {
1134
if (e.key == "Enter") {
1135
e.target.closest("popupnotification").button.doCommand();
1136
}
1137
};
1138
1139
let onVisibilityToggle = commandEvent => {
1140
let passwordField = chromeDoc.getElementById(
1141
"password-notification-password"
1142
);
1143
// Gets the caret position before changing the type of the textbox
1144
let selectionStart = passwordField.selectionStart;
1145
let selectionEnd = passwordField.selectionEnd;
1146
passwordField.setAttribute(
1147
"type",
1148
commandEvent.target.checked ? "" : "password"
1149
);
1150
if (!passwordField.hasAttribute("focused")) {
1151
return;
1152
}
1153
passwordField.selectionStart = selectionStart;
1154
passwordField.selectionEnd = selectionEnd;
1155
};
1156
1157
let persistData = () => {
1158
let foundLogins = LoginHelper.searchLoginsWithObject({
1159
formActionOrigin: login.formActionOrigin,
1160
origin: login.origin,
1161
httpRealm: login.httpRealm,
1162
schemeUpgrades: LoginHelper.schemeUpgrades,
1163
});
1164
1165
let logins = this._filterUpdatableLogins(
1166
login,
1167
foundLogins,
1168
autoSavedLoginGuid
1169
);
1170
let resolveBy = ["scheme", "timePasswordChanged"];
1171
logins = LoginHelper.dedupeLogins(
1172
logins,
1173
["username"],
1174
resolveBy,
1175
login.origin
1176
);
1177
// sort exact username matches to the top
1178
logins.sort(l => (l.username == login.username ? -1 : 1));
1179
1180
this.log(`persistData: Matched ${logins.length} logins`);
1181
1182
let loginToRemove;
1183
let loginToUpdate = logins.shift();
1184
1185
if (logins.length && logins[0].guid == autoSavedLoginGuid) {
1186
loginToRemove = logins.shift();
1187
}
1188
if (logins.length) {
1189
this.log(
1190
"multiple logins, loginToRemove:",
1191
loginToRemove && loginToRemove.guid
1192
);
1193
Cu.reportError("Unexpected match of multiple logins.");
1194
return;
1195
}
1196
1197
if (!loginToUpdate) {
1198
// Create a new login, don't update an original.
1199
// The original login we have been provided with might have its own
1200
// metadata, but we don't want it propagated to the newly created one.
1201
Services.logins.addLogin(
1202
new LoginInfo(
1203
login.origin,
1204
login.formActionOrigin,
1205
login.httpRealm,
1206
login.username,
1207
login.password,
1208
login.usernameField,
1209
login.passwordField
1210
)
1211
);
1212
} else if (
1213
loginToUpdate.password == login.password &&
1214
loginToUpdate.username == login.username
1215
) {
1216
// We only want to touch the login's use count and last used time.
1217
this.log("persistData: Touch matched login", loginToUpdate.guid);
1218
this._updateLogin(loginToUpdate);
1219
} else {
1220
this.log("persistData: Update matched login", loginToUpdate.guid);
1221
this._updateLogin(loginToUpdate, login);
1222
// notify that this auto-saved login been merged
1223
if (loginToRemove && loginToRemove.guid == autoSavedLoginGuid) {
1224
Services.obs.notifyObservers(
1225
loginToRemove,
1226
"passwordmgr-autosaved-login-merged"
1227
);
1228
}
1229
}
1230
1231
if (loginToRemove) {
1232
this.log("persistData: removing login", loginToRemove.guid);
1233
Services.logins.removeLogin(loginToRemove);
1234
}
1235
};
1236
1237
// The main action is the "Save" or "Update" button.
1238
let mainAction = {
1239
label: this._getLocalizedString(initialMsgNames.buttonLabel),
1240
accessKey: this._getLocalizedString(initialMsgNames.buttonAccessKey),
1241
callback: () => {
1242
histogram.add(PROMPT_ADD_OR_UPDATE);
1243
if (histogramName == "PWMGR_PROMPT_REMEMBER_ACTION") {
1244
Services.obs.notifyObservers(browser, "LoginStats:NewSavedPassword");
1245
}
1246
readDataFromUI();
1247
persistData();
1248
Services.obs.notifyObservers(
1249
null,
1250
"weave:telemetry:histogram",
1251
histogramName
1252
);
1253
browser.focus();
1254
},
1255
};
1256
1257
let secondaryActions = [
1258
{
1259
label: this._getLocalizedString(initialMsgNames.secondaryButtonLabel),
1260
accessKey: this._getLocalizedString(
1261
initialMsgNames.secondaryButtonAccessKey
1262
),
1263
callback: () => {
1264
histogram.add(PROMPT_NOTNOW);
1265
Services.obs.notifyObservers(
1266
null,
1267
"weave:telemetry:histogram",
1268
histogramName
1269
);
1270
browser.focus();
1271
},
1272
},
1273
];
1274
// Include a "Never for this site" button when saving a new password.
1275
if (type == "password-save") {
1276
secondaryActions.push({
1277
label: this._getLocalizedString("saveLoginButtonNever.label"),
1278
accessKey: this._getLocalizedString("saveLoginButtonNever.accesskey"),
1279
callback: () => {
1280
histogram.add(PROMPT_NEVER);
1281
Services.obs.notifyObservers(
1282
null,
1283
"weave:telemetry:histogram",
1284
histogramName
1285
);
1286
Services.logins.setLoginSavingEnabled(login.origin, false);
1287
browser.focus();
1288
},
1289
});
1290
}
1291
1292
let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
1293
let togglePasswordLabel = this._getLocalizedString("togglePasswordLabel");
1294
let togglePasswordAccessKey = this._getLocalizedString(
1295
"togglePasswordAccessKey2"
1296
);
1297
1298
let popupNote = this._getPopupNote();
1299
let notificationID = "password";
1300
// keep attention notifications around for longer after a locationchange
1301
const timeoutMs =
1302
showOptions.dismissed && showOptions.extraAttr == "attention"
1303
? ATTENTION_NOTIFICATION_TIMEOUT_MS
1304
: NOTIFICATION_TIMEOUT_MS;
1305
popupNote.show(
1306
browser,
1307
notificationID,
1308
promptMsg,
1309
"password-notification-icon",
1310
mainAction,
1311
secondaryActions,
1312
Object.assign(
1313
{
1314
timeout: Date.now() + timeoutMs,
1315
persistWhileVisible: true,
1316
passwordNotificationType: type,
1317
hideClose: true,
1318
eventCallback(topic) {
1319
switch (topic) {
1320
case "showing":
1321
currentNotification = this;
1322
chromeDoc
1323
.getElementById("password-notification-password")
1324
.removeAttribute("focused");
1325
chromeDoc
1326
.getElementById("password-notification-username")
1327
.removeAttribute("focused");
1328
chromeDoc
1329
.getElementById("password-notification-username")
1330
.addEventListener("input", onInput);
1331
chromeDoc
1332
.getElementById("password-notification-username")
1333
.addEventListener("keyup", onKeyUp);
1334
chromeDoc
1335
.getElementById("password-notification-password")
1336
.addEventListener("keyup", onKeyUp);
1337
chromeDoc
1338
.getElementById("password-notification-password")
1339
.addEventListener("input", onInput);
1340
let toggleBtn = chromeDoc.getElementById(
1341
"password-notification-visibilityToggle"
1342
);
1343
1344
if (
1345
Services.prefs.getBoolPref(
1346
"signon.rememberSignons.visibilityToggle"
1347
)
1348
) {
1349
toggleBtn.addEventListener("command", onVisibilityToggle);
1350
toggleBtn.setAttribute("label", togglePasswordLabel);
1351
toggleBtn.setAttribute("accesskey", togglePasswordAccessKey);
1352
let hideToggle =
1353
LoginHelper.isMasterPasswordSet() ||
1354
// Dismissed-by-default prompts should still show the toggle.
1355
(this.timeShown && this.wasDismissed) ||
1356
// If we are only adding a username then the password is
1357
// one that is already saved and we don't want to reveal
1358
// it as the submitter of this form may not be the account
1359
// owner, they may just be using the saved password.
1360
(messageStringID == "updateLoginMsgAddUsername" &&
1361
login.timePasswordChanged <
1362
Date.now() - VISIBILITY_TOGGLE_MAX_PW_AGE_MS);
1363
toggleBtn.setAttribute("hidden", hideToggle);
1364
}
1365
break;
1366
case "shown": {
1367
writeDataToUI();
1368
let anchorIcon = this.anchorElement;
1369
if (anchorIcon && this.options.extraAttr == "attention") {
1370
anchorIcon.removeAttribute("extraAttr");
1371
delete this.options.extraAttr;
1372
}
1373
break;
1374
}
1375
case "dismissed":
1376
this.wasDismissed = true;
1377
readDataFromUI();
1378
// Fall through.
1379
case "removed":
1380
currentNotification = null;
1381
chromeDoc
1382
.getElementById("password-notification-username")
1383
.removeEventListener("input", onInput);
1384
chromeDoc
1385
.getElementById("password-notification-username")
1386
.removeEventListener("keyup", onKeyUp);
1387
chromeDoc
1388
.getElementById("password-notification-password")
1389
.removeEventListener("input", onInput);
1390
chromeDoc
1391
.getElementById("password-notification-password")
1392
.removeEventListener("keyup", onKeyUp);
1393
chromeDoc
1394
.getElementById("password-notification-visibilityToggle")
1395
.removeEventListener("command", onVisibilityToggle);
1396
break;
1397
}
1398
return false;
1399
},
1400
},
1401
showOptions
1402
)
1403
);
1404
1405
if (notifySaved) {
1406
let notification = popupNote.getNotification(notificationID);
1407
let anchor = notification.anchorElement;
1408
anchor.ownerGlobal.ConfirmationHint.show(anchor, "passwordSaved");
1409
}
1410
},
1411
1412
_removeLoginNotifications() {
1413
var popupNote = this._getPopupNote();
1414
if (popupNote) {
1415
popupNote = popupNote.getNotification("password");
1416
}
1417
if (popupNote) {
1418
popupNote.remove();
1419
}
1420
},
1421
1422
/**
1423
* Called when we detect a new login in a form submission,
1424
* asks the user what to do.
1425
*/
1426
_showSaveLoginDialog(aLogin) {
1427
const buttonFlags =
1428
Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
1429
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
1430
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
1431
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2;
1432
1433
var displayHost = this._getShortDisplayHost(aLogin.origin);
1434
1435
var dialogText;
1436
if (aLogin.username) {
1437
var displayUser = this._sanitizeUsername(aLogin.username);
1438
dialogText = this._getLocalizedString("rememberPasswordMsg", [
1439
displayUser,
1440
displayHost,
1441
]);
1442
} else {
1443
dialogText = this._getLocalizedString("rememberPasswordMsgNoUsername", [
1444
displayHost,
1445
]);
1446
}
1447
var dialogTitle = this._getLocalizedString("savePasswordTitle");
1448
var neverButtonText = this._getLocalizedString("neverForSiteButtonText");
1449
var rememberButtonText = this._getLocalizedString("rememberButtonText");
1450
var notNowButtonText = this._getLocalizedString("notNowButtonText");
1451
1452
this.log("Prompting user to save/ignore login");
1453
var userChoice = Services.prompt.confirmEx(
1454
this._chromeWindow,
1455
dialogTitle,
1456
dialogText,
1457
buttonFlags,
1458
rememberButtonText,
1459
notNowButtonText,
1460
neverButtonText,
1461
null,
1462
{}
1463
);
1464
// Returns:
1465
// 0 - Save the login
1466
// 1 - Ignore the login this time
1467
// 2 - Never save logins for this site
1468
if (userChoice == 2) {
1469
this.log("Disabling " + aLogin.origin + " logins by request.");
1470
Services.logins.setLoginSavingEnabled(aLogin.origin, false);
1471
} else if (userChoice == 0) {
1472
this.log("Saving login for " + aLogin.origin);
1473
Services.logins.addLogin(aLogin);
1474
} else {
1475
// userChoice == 1 --> just ignore the login.
1476
this.log("Ignoring login.");
1477
}
1478
1479
Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
1480
},
1481
1482
/**
1483
* Called when we think we detect a password or username change for
1484
* an existing login, when the form being submitted contains multiple
1485
* password fields.
1486
*
1487
* @param {nsILoginInfo} aOldLogin
1488
* The old login we may want to update.
1489
* @param {nsILoginInfo} aNewLogin
1490
* The new login from the page form.
1491
* @param {boolean} [dismissed = false]
1492
* If the prompt should be automatically dismissed on being shown.
1493
* @param {boolean} [notifySaved = false]
1494
* Whether the notification should indicate that a login has been saved
1495
* @param {string} [autoSavedLoginGuid = ""]
1496
* A guid value for the old login to be removed if the changes match it
1497
* to a different login
1498
*/
1499
promptToChangePassword(
1500
aOldLogin,
1501
aNewLogin,
1502
dismissed = false,
1503
notifySaved = false,
1504
autoSavedLoginGuid = ""
1505
) {
1506
let notifyObj = this._getPopupNote();
1507
1508
if (notifyObj) {
1509
this._showChangeLoginNotification(
1510
notifyObj,
1511
aOldLogin,
1512
aNewLogin,
1513
dismissed,
1514
notifySaved,
1515
autoSavedLoginGuid
1516
);
1517
} else {
1518
this._showChangeLoginDialog(aOldLogin, aNewLogin);
1519
}
1520
},
1521
1522
/**
1523
* Shows the Change Password popup notification.
1524
*
1525
* @param aNotifyObj
1526
* A popup notification.
1527
*
1528
* @param aOldLogin
1529
* The stored login we want to update.
1530
*
1531
* @param aNewLogin
1532
* The login object with the changes we want to make.
1533
* @param dismissed
1534
* A boolean indicating if the prompt should be automatically
1535
* dismissed on being shown.
1536
* @param notifySaved
1537
* A boolean value indicating whether the notification should indicate that
1538
* a login has been saved
1539
*/
1540
_showChangeLoginNotification(
1541
aNotifyObj,
1542
aOldLogin,
1543
aNewLogin,
1544
dismissed = false,
1545
notifySaved = false,
1546
autoSavedLoginGuid = ""
1547
) {
1548
let login = aOldLogin.clone();
1549
login.origin = aNewLogin.origin;
1550
login.formActionOrigin = aNewLogin.formActionOrigin;
1551
login.password = aNewLogin.password;
1552
login.username = aNewLogin.username;
1553
1554
let messageStringID;
1555
if (
1556
aOldLogin.username === "" &&
1557
login.username !== "" &&
1558
login.password == aOldLogin.password
1559
) {
1560
// If the saved password matches the password we're prompting with then we
1561
// are only prompting to let the user add a username since there was one in
1562
// the form. Change the message so the purpose of the prompt is clearer.
1563
messageStringID = "updateLoginMsgAddUsername";
1564
}
1565
1566
this._showLoginCaptureDoorhanger(
1567
login,
1568
"password-change",
1569
{
1570
dismissed,
1571
extraAttr: notifySaved ? "attention" : "",
1572
},
1573
{
1574
notifySaved,
1575
messageStringID,
1576
autoSavedLoginGuid,
1577
}
1578
);
1579
1580
let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
1581
Services.obs.notifyObservers(
1582
aNewLogin,
1583
"passwordmgr-prompt-change",
1584
oldGUID
1585
);
1586
},
1587
1588
/**
1589
* Shows the Change Password dialog.
1590
*/
1591
_showChangeLoginDialog(aOldLogin, aNewLogin) {
1592
const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
1593
1594
var dialogText;
1595
if (aOldLogin.username) {
1596
dialogText = this._getLocalizedString("updatePasswordMsg", [
1597
aOldLogin.username,
1598
]);
1599
} else {
1600
dialogText = this._getLocalizedString("updatePasswordMsgNoUser");
1601
}
1602
1603
var dialogTitle = this._getLocalizedString("passwordChangeTitle");
1604
1605
// returns 0 for yes, 1 for no.
1606
var ok = !Services.prompt.confirmEx(
1607
this._chromeWindow,
1608
dialogTitle,
1609
dialogText,
1610
buttonFlags,
1611
null,
1612
null,
1613
null,
1614
null,
1615
{}
1616
);
1617
if (ok) {
1618
this.log("Updating password for user " + aOldLogin.username);
1619
this._updateLogin(aOldLogin, aNewLogin);
1620
}
1621
1622
let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
1623
Services.obs.notifyObservers(
1624
aNewLogin,
1625
"passwordmgr-prompt-change",
1626
oldGUID
1627
);
1628
},
1629
1630
/**
1631
* Called when we detect a password change in a form submission, but we
1632
* don't know which existing login (username) it's for. Asks the user
1633
* to select a username and confirm the password change.
1634
*
1635
* Note: The caller doesn't know the username for aNewLogin, so this
1636
* function fills in .username and .usernameField with the values
1637
* from the login selected by the user.
1638
*/
1639
promptToChangePasswordWithUsernames(logins, aNewLogin) {
1640
this.log("promptToChangePasswordWithUsernames with count:", logins.length);
1641
1642
var usernames = logins.map(
1643
l => l.username || this._getLocalizedString("noUsername")
1644
);
1645
var dialogText = this._getLocalizedString("userSelectText2");
1646
var dialogTitle = this._getLocalizedString("passwordChangeTitle");
1647
var selectedIndex = { value: null };
1648
1649
// If user selects ok, outparam.value is set to the index
1650
// of the selected username.
1651
var ok = Services.prompt.select(
1652
this._chromeWindow,
1653
dialogTitle,
1654
dialogText,
1655
usernames,
1656
selectedIndex
1657
);
1658
if (ok) {
1659
// Now that we know which login to use, modify its password.
1660
var selectedLogin = logins[selectedIndex.value];
1661
this.log("Updating password for user " + selectedLogin.username);
1662
var newLoginWithUsername = Cc[
1663
"@mozilla.org/login-manager/loginInfo;1"
1664
].createInstance(Ci.nsILoginInfo);
1665
newLoginWithUsername.init(
1666
aNewLogin.origin,
1667
aNewLogin.formActionOrigin,
1668
aNewLogin.httpRealm,
1669
selectedLogin.username,
1670
aNewLogin.password,
1671
selectedLogin.usernameField,
1672
aNewLogin.passwordField
1673
);
1674
this._updateLogin(selectedLogin, newLoginWithUsername);
1675
}
1676
},
1677
1678
/* ---------- Internal Methods ---------- */
1679
1680
_updateLogin(login, aNewLogin = null) {
1681
var now = Date.now();
1682
var propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
1683
Ci.nsIWritablePropertyBag
1684
);
1685
if (aNewLogin) {
1686
propBag.setProperty("formActionOrigin", aNewLogin.formActionOrigin);
1687
propBag.setProperty("origin", aNewLogin.origin);
1688
propBag.setProperty("password", aNewLogin.password);
1689
propBag.setProperty("username", aNewLogin.username);
1690
// Explicitly set the password change time here (even though it would
1691
// be changed automatically), to ensure that it's exactly the same
1692
// value as timeLastUsed.
1693
propBag.setProperty("timePasswordChanged", now);
1694
}
1695
propBag.setProperty("timeLastUsed", now);
1696
propBag.setProperty("timesUsedIncrement", 1);
1697
Services.logins.modifyLogin(login, propBag);
1698
},
1699
1700
/**
1701
* Given a content DOM window, returns the chrome window and browser it's in.
1702
*/
1703
_getChromeWindow(aWindow) {
1704
// Handle non-e10s toolkit consumers.
1705
if (!Cu.isCrossProcessWrapper(aWindow)) {
1706
let browser = aWindow.docShell.chromeEventHandler;
1707
if (!browser) {
1708
return null;
1709
}
1710
1711
let chromeWin = browser.ownerGlobal;
1712
if (!chromeWin) {
1713
return null;
1714
}
1715
1716
return { win: chromeWin, browser };
1717
}
1718
1719
return null;
1720
},
1721
1722
_getNotifyWindow() {
1723
// Some sites pop up a temporary login window, which disappears
1724
// upon submission of credentials. We want to put the notification
1725
// prompt in the opener window if this seems to be happening.
1726
if (this._openerBrowser) {
1727
let chromeDoc = this._chromeWindow.document.documentElement;
1728
1729
// Check to see if the current window was opened with chrome
1730
// disabled, and if so use the opener window. But if the window
1731
// has been used to visit other pages (ie, has a history),
1732
// assume it'll stick around and *don't* use the opener.
1733
if (chromeDoc.getAttribute("chromehidden") && !this._browser.canGoBack) {
1734
this.log("Using opener window for notification prompt.");
1735
return {
1736
win: this._openerBrowser.ownerGlobal,
1737
browser: this._openerBrowser,
1738
};
1739
}
1740
}
1741
1742
return {
1743
win: this._chromeWindow,
1744
browser: this._browser,
1745
};
1746
},
1747
1748
/**
1749
* Returns the popup notification to this prompter,
1750
* or null if there isn't one available.
1751
*/
1752
_getPopupNote() {
1753
let popupNote = null;
1754
1755
try {
1756
let { win: notifyWin } = this._getNotifyWindow();
1757
1758
// .wrappedJSObject needed here -- see bug 422974 comment 5.
1759
popupNote = notifyWin.wrappedJSObject.PopupNotifications;
1760
} catch (e) {
1761
this.log("Popup notifications not available on window");
1762
}
1763
1764
return popupNote;
1765
},
1766
1767
/**
1768
* The user might enter a login that isn't the one we prefilled, but
1769
* is the same as some other existing login. So, pick a login with a
1770
* matching username, or return null.
1771
*/
1772
_repickSelectedLogin(foundLogins, username) {
1773
for (var i = 0; i < foundLogins.length; i++) {
1774
if (foundLogins[i].username == username) {
1775
return foundLogins[i];
1776
}
1777
}
1778
return null;
1779
},
1780
1781
/**
1782
* Can be called as:
1783
* _getLocalizedString("key1");
1784
* _getLocalizedString("key2", ["arg1"]);
1785
* _getLocalizedString("key3", ["arg1", "arg2"]);
1786
* (etc)
1787
*
1788
* Returns the localized string for the specified key,
1789
* formatted if required.
1790
*
1791
*/
1792
_getLocalizedString(key, formatArgs) {
1793
if (formatArgs) {
1794
return this._strBundle.formatStringFromName(key, formatArgs);
1795
}
1796
return this._strBundle.GetStringFromName(key);
1797
},
1798
1799
/**
1800
* Sanitizes the specified username, by stripping quotes and truncating if
1801
* it's too long. This helps prevent an evil site from messing with the
1802
* "save password?" prompt too much.
1803
*/
1804
_sanitizeUsername(username) {
1805
if (username.length > 30) {
1806
username = username.substring(0, 30);
1807
username += this._ellipsis;
1808
}
1809
return username.replace(/['"]/g, "");